diff --git a/.gitignore b/.gitignore index ef02830..f0ad69f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,8 @@ *.DS_Store *.idea/ *.vscode/ +node_modules/ + *.env.local *.github/ *.opencode/ diff --git a/.opencode/antigravity.json b/.opencode/antigravity.json new file mode 100644 index 0000000..0ca83a0 --- /dev/null +++ b/.opencode/antigravity.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/NoeFabris/opencode-antigravity-auth/main/assets/antigravity.schema.json", + "quiet_mode": false, + "debug": false, + "auto_update": true, + "keep_thinking": false, + "session_recovery": true, + "auto_resume": true, + "resume_text": "continue", + "empty_response_max_attempts": 4, + "empty_response_retry_delay_ms": 2000, + "tool_id_recovery": true, + "claude_tool_hardening": true, + "proactive_token_refresh": true, + "proactive_refresh_buffer_seconds": 1800, + "proactive_refresh_check_interval_seconds": 300, + "max_rate_limit_wait_seconds": 300, + "quota_fallback": false, + "account_selection_strategy": "sticky", + "pid_offset_enabled": false, + "signature_cache": { + "enabled": true, + "memory_ttl_seconds": 3600, + "disk_ttl_seconds": 172800, + "write_interval_seconds": 60 + } +} \ No newline at end of file diff --git a/.opencode/opencode.json b/.opencode/opencode.json new file mode 100644 index 0000000..b2cb823 --- /dev/null +++ b/.opencode/opencode.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://opencode.ai/config.json", + "plugin": ["opencode-antigravity-auth@beta"], + "provider": { + "google": { + "models": { + "antigravity-gemini-3-pro": { + "name": "Gemini 3 Pro (Antigravity)", + "limit": { "context": 1048576, "output": 65535 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }, + "variants": { + "low": { "thinkingLevel": "low" }, + "high": { "thinkingLevel": "high" } + } + }, + "antigravity-gemini-3-flash": { + "name": "Gemini 3 Flash (Antigravity)", + "limit": { "context": 1048576, "output": 65536 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }, + "variants": { + "minimal": { "thinkingLevel": "minimal" }, + "low": { "thinkingLevel": "low" }, + "medium": { "thinkingLevel": "medium" }, + "high": { "thinkingLevel": "high" } + } + }, + "antigravity-claude-sonnet-4-5": { + "name": "Claude Sonnet 4.5 (Antigravity)", + "limit": { "context": 200000, "output": 64000 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } + }, + "antigravity-claude-sonnet-4-5-thinking": { + "name": "Claude Sonnet 4.5 Thinking (Antigravity)", + "limit": { "context": 200000, "output": 64000 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }, + "variants": { + "low": { "thinkingConfig": { "thinkingBudget": 8192 } }, + "max": { "thinkingConfig": { "thinkingBudget": 32768 } } + } + }, + "antigravity-claude-opus-4-5-thinking": { + "name": "Claude Opus 4.5 Thinking (Antigravity)", + "limit": { "context": 200000, "output": 64000 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }, + "variants": { + "low": { "thinkingConfig": { "thinkingBudget": 8192 } }, + "max": { "thinkingConfig": { "thinkingBudget": 32768 } } + } + }, + "gemini-2.5-flash": { + "name": "Gemini 2.5 Flash (Gemini CLI)", + "limit": { "context": 1048576, "output": 65536 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } + }, + "gemini-2.5-pro": { + "name": "Gemini 2.5 Pro (Gemini CLI)", + "limit": { "context": 1048576, "output": 65536 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } + }, + "gemini-3-flash-preview": { + "name": "Gemini 3 Flash Preview (Gemini CLI)", + "limit": { "context": 1048576, "output": 65536 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } + }, + "gemini-3-pro-preview": { + "name": "Gemini 3 Pro Preview (Gemini CLI)", + "limit": { "context": 1048576, "output": 65535 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } + } + } + } + } +} \ No newline at end of file diff --git a/.opencode/package-lock.json b/.opencode/package-lock.json new file mode 100644 index 0000000..5151efe --- /dev/null +++ b/.opencode/package-lock.json @@ -0,0 +1,389 @@ +{ + "name": ".opencode", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@kilocode/plugin": "7.2.22", + "@opencode-ai/plugin": "1.1.31" + } + }, + "node_modules/@kilocode/plugin": { + "version": "7.2.22", + "resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.22.tgz", + "integrity": "sha512-uS8tnoLzXAyDHHgSOvP/GhrvkKpus6i6tmWb57E4+YfgHBOO7HqF+LzV4MiC1cuGIifbMtyUEi3kZWmWdIuhhw==", + "license": "MIT", + "dependencies": { + "@kilocode/sdk": "7.2.22", + "effect": "4.0.0-beta.48", + "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.1.100", + "@opentui/solid": ">=0.1.100" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + } + }, + "node_modules/@kilocode/sdk": { + "version": "7.2.22", + "resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.22.tgz", + "integrity": "sha512-2t4VuK5rVY9o/Pck/oRJ+CxAAqnwLhRAD/i91uSabWw4POGlOHHsq2etQKFAX8kJ5zdTk/I1DLvffh7bFPPXZw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "7.0.6" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@opencode-ai/plugin": { + "version": "1.1.31", + "license": "MIT", + "dependencies": { + "@opencode-ai/sdk": "1.1.31", + "zod": "4.1.8" + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.1.31", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/effect": { + "version": "4.0.0-beta.48", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz", + "integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "fast-check": "^4.6.0", + "find-my-way-ts": "^0.1.6", + "ini": "^6.0.0", + "kubernetes-types": "^1.30.0", + "msgpackr": "^1.11.9", + "multipasta": "^0.2.7", + "toml": "^4.1.1", + "uuid": "^13.0.0", + "yaml": "^2.8.3" + } + }, + "node_modules/fast-check": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz", + "integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/find-my-way-ts": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", + "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", + "license": "MIT" + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/kubernetes-types": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", + "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", + "license": "Apache-2.0" + }, + "node_modules/msgpackr": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.10.tgz", + "integrity": "sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multipasta": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz", + "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==", + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pure-rand": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", + "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/toml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz", + "integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.1.8", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/Convoi.json b/Convoi.json new file mode 100644 index 0000000..703821f --- /dev/null +++ b/Convoi.json @@ -0,0 +1,291 @@ +{ + "defuntId": null, + "titreMission": "", + "clientId": null, + "typeConvoi": "local", + "lieuDepart": { + "modeSelection": "lieu", + "lieuId": null, + "nom": "", + "adresse": "", + "ville": "", + "codePostal": "", + "pays": "", + "latitude": null, + "longitude": null, + "detailsComplementaires": "" + }, + "modeTransport": "route", + "debutPrevu": "2026-04-15T10:57", + "finEstimee": null, + "statut": "planifie", + "emailFamille": "", + "notificationsAutomatiques": true, + "ongletsMission": { + "itineraire": { + "enabled": false + }, + "documentsLegaux": { + "enabled": false + }, + "equipeMoyens": { + "enabled": true + }, + "demarches": { + "enabled": true + }, + "suiviCouts": { + "enabled": true + }, + "carburant": { + "enabled": true + }, + "ceremonie": { + "enabled": true + }, + "thanatopraxie": { + "enabled": true + }, + "suiviGpsEtapes": { + "enabled": false + }, + "communication": { + "enabled": true + } + } +} + + +// VEHICULE +{ + "attributMissionConvoi": { + "defuntId": null, + "titreMission": "", + "clientId": null, + "typeConvoi": "local", + "lieuDepart": { + "modeSelection": "lieu", + "lieuId": null, + "nom": "", + "adresse": "", + "ville": "", + "codePostal": "", + "pays": "", + "latitude": null, + "longitude": null, + "detailsComplementaires": "" + }, + "modeTransport": "route", + "debutPrevu": "2026-04-15T10:57", + "finEstimee": null, + "statut": "planifie", + "emailFamille": "", + "notificationsAutomatiques": true, + "ongletsMission": { + "itineraire": false, + "documentsLegaux": false, + "equipeMoyens": true, + "demarches": true, + "suiviCouts": true, + "carburant": true, + "ceremonie": true, + "thanatopraxie": true, + "suiviGpsEtapes": false, + "communication": true + } + }, + "attributVehicule": { + "photoVehicule": { + "fileName": "", + "fileUrl": "", + "mimeType": "", + "size": null + }, + "marque": "", + "modele": "", + "immatriculation": "", + "typeVehicule": "utilitaire", + "carburant": "diesel", + "annee": 2026, + "utilisateurPrincipalId": null, + "statut": "actif", + "notes": "", + "ongletsVehicule": { + "informationsGenerales": true, + "entretienMaintenance": true, + "coutsAcquisition": true + } + } +} + +{ + "attributMissionConvoi": { + "defunt": { + "label": "Défunt", + "required": true, + "type": "select", + "value": null + }, + "titreMission": { + "label": "Titre de la mission", + "required": false, + "type": "string", + "value": "" + }, + "client": { + "label": "Client (Donneur d'ordre)", + "required": false, + "type": "select", + "value": null + }, + "typeConvoi": { + "label": "Type de convoi", + "required": false, + "type": "select", + "value": "Local" + }, + "lieuDepartConvoi": { + "label": "Lieu de Départ du Convoi", + "required": false, + "type": "object", + "value": { + "mode": "selection_lieu", + "recherche": "", + "lieuId": null, + "adresseManuelle": null + } + }, + "modeTransport": { + "label": "Mode de Transport", + "required": false, + "type": "select", + "value": "Route" + }, + "debutPrevu": { + "label": "Début Prévu", + "required": true, + "type": "datetime-local", + "value": "2026-04-15T10:57" + }, + "finEstimee": { + "label": "Fin Estimée", + "required": false, + "type": "datetime-local", + "value": null + }, + "statut": { + "label": "Statut", + "required": false, + "type": "select", + "value": "Planifié" + }, + "emailFamille": { + "label": "Email famille (pour notifications auto)", + "required": false, + "type": "email", + "value": "" + }, + "notificationsAutomatiques": { + "label": "Notifications automatiques (départ, arrivée, frontière)", + "required": false, + "type": "boolean", + "value": true + } + }, + "attributVehicule": { + "photoVehicule": { + "label": "Photo du véhicule", + "required": false, + "type": "file:image", + "value": null + }, + "marque": { + "label": "Marque", + "required": true, + "type": "select", + "value": null, + "options": [ + "Mercedes-Benz", + "Peugeot", + "Renault", + "Citroën", + "Volkswagen", + "Ford", + "Fiat", + "Opel", + "Toyota", + "Nissan", + "Volvo", + "BMW", + "Audi", + "Iveco", + "Autre" + ] + }, + "modele": { + "label": "Modèle", + "required": true, + "type": "string", + "value": "" + }, + "immatriculation": { + "label": "Immatriculation", + "required": true, + "type": "string", + "value": "" + }, + "typeVehicule": { + "label": "Type véhicule", + "required": false, + "type": "select", + "value": "utilitaire", + "options": [ + "corbillard", + "vehicule_transport", + "utilitaire", + "berline" + ] + }, + "carburant": { + "label": "Carburant", + "required": false, + "type": "select", + "value": "diesel", + "options": [ + "diesel", + "essence", + "electrique", + "hybride" + ] + }, + "annee": { + "label": "Année", + "required": false, + "type": "number", + "value": 2026 + }, + "utilisateurPrincipal": { + "label": "Utilisateur principal", + "required": false, + "type": "select", + "value": null + }, + "statutVehicule": { + "label": "Statut", + "required": false, + "type": "select", + "value": "actif", + "options": [ + "actif", + "en_maintenance", + "hors_service" + ] + }, + "notes": { + "label": "Notes", + "required": false, + "type": "textarea", + "value": "" + } + } +} \ No newline at end of file diff --git a/New-Thanasoft_MVP_Tracking.xlsx b/New-Thanasoft_MVP_Tracking.xlsx new file mode 100644 index 0000000..acc3006 Binary files /dev/null and b/New-Thanasoft_MVP_Tracking.xlsx differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2767a5b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1000 @@ +{ + "name": "New-Thanasoft", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "exceljs": "^4.4.0" + } + }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fa6a38a --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "exceljs": "^4.4.0" + } +} diff --git a/thanasoft-back/.env.example b/thanasoft-back/.env.example deleted file mode 100644 index e7e7b47..0000000 --- a/thanasoft-back/.env.example +++ /dev/null @@ -1,65 +0,0 @@ -APP_NAME=Laravel -APP_ENV=local -APP_KEY= -APP_DEBUG=true -APP_URL=http://localhost - -APP_LOCALE=en -APP_FALLBACK_LOCALE=en -APP_FAKER_LOCALE=en_US - -APP_MAINTENANCE_DRIVER=file -# APP_MAINTENANCE_STORE=database - -PHP_CLI_SERVER_WORKERS=4 - -BCRYPT_ROUNDS=12 - -LOG_CHANNEL=stack -LOG_STACK=single -LOG_DEPRECATIONS_CHANNEL=null -LOG_LEVEL=debug - -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=thanasoft_back -DB_USERNAME=root -DB_PASSWORD= - -SESSION_DRIVER=database -SESSION_LIFETIME=120 -SESSION_ENCRYPT=false -SESSION_PATH=/ -SESSION_DOMAIN=null - -BROADCAST_CONNECTION=log -FILESYSTEM_DISK=local -QUEUE_CONNECTION=database - -CACHE_STORE=database -# CACHE_PREFIX= - -MEMCACHED_HOST=127.0.0.1 - -REDIS_CLIENT=phpredis -REDIS_HOST=127.0.0.1 -REDIS_PASSWORD=null -REDIS_PORT=6379 - -MAIL_MAILER=log -MAIL_SCHEME=null -MAIL_HOST=127.0.0.1 -MAIL_PORT=2525 -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_FROM_ADDRESS="hello@example.com" -MAIL_FROM_NAME="${APP_NAME}" - -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_DEFAULT_REGION=us-east-1 -AWS_BUCKET= -AWS_USE_PATH_STYLE_ENDPOINT=false - -VITE_APP_NAME="${APP_NAME}" diff --git a/thanasoft-back/API_DOCUMENTATION.md b/thanasoft-back/API_DOCUMENTATION.md new file mode 100644 index 0000000..e9d846e --- /dev/null +++ b/thanasoft-back/API_DOCUMENTATION.md @@ -0,0 +1,656 @@ +# API Documentation - Employee Management System + +## Overview + +The ThanaSoft Employee Management System provides comprehensive RESTful API endpoints for managing employees, thanatopractitioners, and their associated documents. This system is built using Laravel and follows RESTful API conventions. + +## Base URL + +``` +https://your-domain.com/api +``` + +## Authentication + +All API endpoints require authentication using Laravel Sanctum. Include the token in the Authorization header: + +``` +Authorization: Bearer {your_token} +``` + +## Employee Management System + +### Entities + +1. **Employees** - Core employee records with personal information +2. **Thanatopractitioners** - Specialized practitioners linked to employees +3. **Practitioner Documents** - Documents associated with thanatopractitioners + +--- + +## Employees API + +### Base Endpoint + +``` +/employees +``` + +### Endpoints Overview + +| Method | Endpoint | Description | +| ------ | --------------------------------- | ----------------------------------------------- | +| GET | `/employees` | List all employees with pagination | +| POST | `/employees` | Create a new employee | +| GET | `/employees/{id}` | Get specific employee details | +| PUT | `/employees/{id}` | Update employee information | +| DELETE | `/employees/{id}` | Delete an employee | +| GET | `/employees/searchBy` | Search employees by criteria | +| GET | `/employees/thanatopractitioners` | Get all thanatopractitioners with employee info | + +### 1. List All Employees + +**GET** `/api/employees` + +**Query Parameters:** + +- `page` (optional): Page number for pagination (default: 1) +- `per_page` (optional): Items per page (default: 15, max: 100) +- `search` (optional): Search term for name, email, or employee_number +- `department` (optional): Filter by department +- `status` (optional): Filter by employment status (active, inactive, terminated) + +**Success Response (200):** + +```json +{ + "success": true, + "data": { + "data": [ + { + "id": 1, + "employee_number": "EMP001", + "first_name": "Jean", + "last_name": "Dupont", + "email": "jean.dupont@thanasoft.com", + "phone": "+261 34 12 345 67", + "department": "Direction", + "position": "Directeur Général", + "hire_date": "2020-01-15", + "status": "active", + "created_at": "2025-11-05T10:30:00.000000Z", + "updated_at": "2025-11-05T10:30:00.000000Z" + } + ], + "current_page": 1, + "per_page": 15, + "total": 25, + "last_page": 2 + }, + "message": "Employés récupérés avec succès" +} +``` + +### 2. Create Employee + +**POST** `/api/employees` + +**Request Body:** + +```json +{ + "employee_number": "EMP026", + "first_name": "Jean", + "last_name": "Dupont", + "email": "jean.dupont@thanasoft.com", + "phone": "+261 34 12 345 67", + "address": "123 Rue de la Liberté, Antananarivo", + "birth_date": "1985-06-15", + "gender": "male", + "department": "Direction", + "position": "Directeur Général", + "hire_date": "2025-11-05", + "salary": 2500000, + "status": "active" +} +``` + +**Success Response (201):** + +```json +{ + "success": true, + "data": { + "id": 26, + "employee_number": "EMP026", + "first_name": "Jean", + "last_name": "Dupont", + "email": "jean.dupont@thanasoft.com", + "phone": "+261 34 12 345 67", + "address": "123 Rue de la Liberté, Antananarivo", + "birth_date": "1985-06-15", + "gender": "male", + "department": "Direction", + "position": "Directeur Général", + "hire_date": "2025-11-05", + "salary": 2500000, + "status": "active", + "created_at": "2025-11-05T12:16:05.000000Z", + "updated_at": "2025-11-05T12:16:05.000000Z" + }, + "message": "Employé créé avec succès" +} +``` + +**Validation Errors (422):** + +```json +{ + "success": false, + "message": "Les données fournies ne sont pas valides", + "errors": { + "email": ["L'adresse email doit être unique"], + "employee_number": ["Le numéro d'employé est requis"] + } +} +``` + +### 3. Get Employee Details + +**GET** `/api/employees/{id}` + +**Success Response (200):** + +```json +{ + "success": true, + "data": { + "id": 1, + "employee_number": "EMP001", + "first_name": "Jean", + "last_name": "Dupont", + "email": "jean.dupont@thanasoft.com", + "phone": "+261 34 12 345 67", + "address": "123 Rue de la Liberté, Antananarivo", + "birth_date": "1985-06-15", + "gender": "male", + "department": "Direction", + "position": "Directeur Général", + "hire_date": "2020-01-15", + "salary": 2500000, + "status": "active", + "created_at": "2020-01-15T08:00:00.000000Z", + "updated_at": "2025-11-05T10:30:00.000000Z" + }, + "message": "Détails de l'employé récupérés avec succès" +} +``` + +### 4. Update Employee + +**PUT** `/api/employees/{id}` + +**Request Body:** Same as create, but all fields are optional. + +**Success Response (200):** + +```json +{ + "success": true, + "data": { + "id": 1, + "employee_number": "EMP001", + "first_name": "Jean", + "last_name": "Dupont", + "email": "jean.dupont@thanasoft.com", + "phone": "+261 34 12 345 68", + "department": "Direction", + "position": "Directeur Général Adjoint", + "updated_at": "2025-11-05T12:16:05.000000Z" + }, + "message": "Employé mis à jour avec succès" +} +``` + +### 5. Search Employees + +**GET** `/api/employees/searchBy` + +**Query Parameters:** + +- `query` (required): Search term +- `field` (optional): Field to search in (first_name, last_name, email, employee_number, department, position) + +**Example:** `/api/employees/searchBy?query=jean&field=first_name` + +**Success Response (200):** + +```json +{ + "success": true, + "data": [ + { + "id": 1, + "employee_number": "EMP001", + "first_name": "Jean", + "last_name": "Dupont", + "email": "jean.dupont@thanasoft.com", + "department": "Direction", + "position": "Directeur Général" + } + ], + "message": "Résultats de recherche récupérés avec succès" +} +``` + +--- + +## Thanatopractitioners API + +### Base Endpoint + +``` +/thanatopractitioners +``` + +### Endpoints Overview + +| Method | Endpoint | Description | +| ------ | ---------------------------------------------- | ------------------------------------ | +| GET | `/thanatopractitioners` | List all thanatopractitioners | +| POST | `/thanatopractitioners` | Create a new thanatopractitioner | +| GET | `/thanatopractitioners/{id}` | Get specific thanatopractitioner | +| PUT | `/thanatopractitioners/{id}` | Update thanatopractitioner | +| DELETE | `/thanatopractitioners/{id}` | Delete thanatopractitioner | +| GET | `/employees/{employeeId}/thanatopractitioners` | Get thanatopractitioners by employee | + +### 1. List All Thanatopractitioners + +**GET** `/api/thanatopractitioners` + +**Query Parameters:** + +- `page` (optional): Page number +- `per_page` (optional): Items per page +- `specialization` (optional): Filter by specialization + +**Success Response (200):** + +```json +{ + "success": true, + "data": { + "data": [ + { + "id": 1, + "employee_id": 1, + "specialization": "Thanatopraticien principal", + "license_number": "TH001", + "authorization_number": "AUTH001", + "authorization_date": "2020-01-15", + "authorization_expiry": "2025-01-15", + "is_authorized": true, + "created_at": "2020-01-15T08:00:00.000000Z", + "updated_at": "2025-11-05T10:30:00.000000Z", + "employee": { + "id": 1, + "first_name": "Jean", + "last_name": "Dupont", + "employee_number": "EMP001", + "department": "Direction" + } + } + ], + "current_page": 1, + "per_page": 15, + "total": 5, + "last_page": 1 + }, + "message": "Thanatopraticiens récupérés avec succès" +} +``` + +### 2. Create Thanatopractitioner + +**POST** `/api/thanatopractitioners` + +**Request Body:** + +```json +{ + "employee_id": 1, + "specialization": "Thanatopraticien principal", + "license_number": "TH001", + "authorization_number": "AUTH001", + "authorization_date": "2025-11-05", + "authorization_expiry": "2026-11-05", + "is_authorized": true +} +``` + +**Success Response (201):** + +```json +{ + "success": true, + "data": { + "id": 6, + "employee_id": 1, + "specialization": "Thanatopraticien principal", + "license_number": "TH001", + "authorization_number": "AUTH001", + "authorization_date": "2025-11-05", + "authorization_expiry": "2026-11-05", + "is_authorized": true, + "created_at": "2025-11-05T12:16:05.000000Z", + "updated_at": "2025-11-05T12:16:05.000000Z" + }, + "message": "Thanatopraticien créé avec succès" +} +``` + +### 3. Get Thanatopractitioners by Employee + +**GET** `/api/employees/{employeeId}/thanatopractitioners` + +**Success Response (200):** + +```json +{ + "success": true, + "data": [ + { + "id": 1, + "specialization": "Thanatopraticien principal", + "license_number": "TH001", + "authorization_number": "AUTH001", + "authorization_date": "2020-01-15", + "authorization_expiry": "2025-01-15", + "is_authorized": true + } + ], + "message": "Thanatopraticiens de l'employé récupérés avec succès" +} +``` + +--- + +## Practitioner Documents API + +### Base Endpoint + +``` +/practitioner-documents +``` + +### Endpoints Overview + +| Method | Endpoint | Description | +| ------ | -------------------------------------- | ------------------------------------ | +| GET | `/practitioner-documents` | List all documents | +| POST | `/practitioner-documents` | Upload new document | +| GET | `/practitioner-documents/{id}` | Get specific document | +| PUT | `/practitioner-documents/{id}` | Update document info | +| DELETE | `/practitioner-documents/{id}` | Delete document | +| GET | `/practitioner-documents/searchBy` | Search documents | +| GET | `/practitioner-documents/expiring` | Get expiring documents | +| GET | `/thanatopractitioners/{id}/documents` | Get documents by thanatopractitioner | +| PATCH | `/practitioner-documents/{id}/verify` | Verify document | + +### 1. List All Documents + +**GET** `/api/practitioner-documents` + +**Query Parameters:** + +- `page` (optional): Page number +- `per_page` (optional): Items per page +- `type` (optional): Filter by document type +- `is_verified` (optional): Filter by verification status + +**Success Response (200):** + +```json +{ + "success": true, + "data": { + "data": [ + { + "id": 1, + "thanatopractitioner_id": 1, + "document_type": "diplome", + "file_name": "diplome_thanatopraticien.pdf", + "file_path": "/documents/diplome_thanatopraticien.pdf", + "issue_date": "2019-06-30", + "expiry_date": "2024-06-30", + "issuing_authority": "Ministère de la Santé", + "is_verified": true, + "verified_at": "2020-01-20T10:00:00.000000Z", + "created_at": "2020-01-15T08:00:00.000000Z", + "updated_at": "2020-01-20T10:00:00.000000Z" + } + ], + "current_page": 1, + "per_page": 15, + "total": 12, + "last_page": 1 + }, + "message": "Documents récupérés avec succès" +} +``` + +### 2. Upload Document + +**POST** `/api/practitioner-documents` + +**Form Data:** + +``` +thanatopractitioner_id: 1 +document_type: diplome +file: [binary file data] +issue_date: 2019-06-30 +expiry_date: 2024-06-30 +issuing_authority: Ministère de la Santé +``` + +**Success Response (201):** + +```json +{ + "success": true, + "data": { + "id": 13, + "thanatopractitioner_id": 1, + "document_type": "diplome", + "file_name": "diplome_thanatopraticien_2025.pdf", + "file_path": "/documents/diplome_thanatopraticien_2025.pdf", + "issue_date": "2019-06-30", + "expiry_date": "2024-06-30", + "issuing_authority": "Ministère de la Santé", + "is_verified": false, + "created_at": "2025-11-05T12:16:05.000000Z", + "updated_at": "2025-11-05T12:16:05.000000Z" + }, + "message": "Document téléchargé avec succès" +} +``` + +### 3. Get Expiring Documents + +**GET** `/api/practitioner-documents/expiring` + +**Query Parameters:** + +- `days` (optional): Number of days to look ahead (default: 30) + +**Success Response (200):** + +```json +{ + "success": true, + "data": [ + { + "id": 5, + "thanatopractitioner_id": 2, + "document_type": "certificat", + "file_name": "certificat_renouvellement.pdf", + "expiry_date": "2025-11-20", + "days_until_expiry": 15, + "employee": { + "first_name": "Marie", + "last_name": "Rasoa", + "employee_number": "EMP002" + } + } + ], + "message": "Documents expirants récupérés avec succès" +} +``` + +### 4. Verify Document + +**PATCH** `/api/practitioner-documents/{id}/verify` + +**Success Response (200):** + +```json +{ + "success": true, + "data": { + "id": 13, + "document_type": "diplome", + "is_verified": true, + "verified_at": "2025-11-05T12:20:00.000000Z" + }, + "message": "Document vérifié avec succès" +} +``` + +--- + +## Data Models + +### Employee Model + +| Field | Type | Required | Description | +| ----------------- | ------- | -------- | ------------------------------------------------ | +| `employee_number` | string | Yes | Unique employee identifier | +| `first_name` | string | Yes | Employee's first name | +| `last_name` | string | Yes | Employee's last name | +| `email` | email | Yes | Unique email address | +| `phone` | string | No | Phone number | +| `address` | text | No | Physical address | +| `birth_date` | date | No | Date of birth | +| `gender` | string | No | Gender (male, female, other) | +| `department` | string | No | Department name | +| `position` | string | No | Job position | +| `hire_date` | date | No | Employment start date | +| `salary` | decimal | No | Monthly salary | +| `status` | string | No | Employment status (active, inactive, terminated) | + +### Thanatopractitioner Model + +| Field | Type | Required | Description | +| ---------------------- | ------- | -------- | ------------------------------ | +| `employee_id` | integer | Yes | Foreign key to employees table | +| `specialization` | string | Yes | Area of specialization | +| `license_number` | string | Yes | Professional license number | +| `authorization_number` | string | Yes | Authorization number | +| `authorization_date` | date | Yes | Authorization issue date | +| `authorization_expiry` | date | Yes | Authorization expiry date | +| `is_authorized` | boolean | Yes | Authorization status | + +### Practitioner Document Model + +| Field | Type | Required | Description | +| ------------------------ | --------- | -------- | ----------------------------------------- | +| `thanatopractitioner_id` | integer | Yes | Foreign key to thanatopractitioners table | +| `document_type` | string | Yes | Type of document | +| `file` | file | Yes | Document file upload | +| `issue_date` | date | No | Document issue date | +| `expiry_date` | date | No | Document expiry date | +| `issuing_authority` | string | No | Authority that issued the document | +| `is_verified` | boolean | Yes | Verification status | +| `verified_at` | timestamp | No | Verification timestamp | + +--- + +## Error Handling + +### HTTP Status Codes + +- `200` - Success +- `201` - Created successfully +- `400` - Bad Request +- `401` - Unauthorized +- `403` - Forbidden +- `404` - Resource not found +- `422` - Validation error +- `500` - Internal server error + +### Error Response Format + +```json +{ + "success": false, + "message": "Description of the error", + "errors": { + "field_name": ["Error message for this field"] + } +} +``` + +--- + +## File Upload + +For document uploads, use multipart/form-data: + +``` +POST /api/practitioner-documents +Content-Type: multipart/form-data + +{ + "thanatopractitioner_id": 1, + "document_type": "diplome", + "file": [binary file], + "issue_date": "2019-06-30", + "expiry_date": "2024-06-30", + "issuing_authority": "Ministère de la Santé" +} +``` + +--- + +## Pagination + +All list endpoints support pagination with the following query parameters: + +- `page`: Page number (default: 1) +- `per_page`: Items per page (default: 15, max: 100) + +Response includes pagination metadata: + +```json +{ + "current_page": 1, + "per_page": 15, + "total": 50, + "last_page": 4, + "from": 1, + "to": 15 +} +``` + +--- + +## Rate Limiting + +API requests are rate limited to 1000 requests per hour per authenticated user. Exceeding this limit will result in a `429 Too Many Requests` response. + +--- + +## Support + +For API support or questions, please contact the development team or refer to the Laravel documentation at https://laravel.com/docs. diff --git a/thanasoft-back/FILE_API_DOCUMENTATION.md b/thanasoft-back/FILE_API_DOCUMENTATION.md new file mode 100644 index 0000000..a2a244e --- /dev/null +++ b/thanasoft-back/FILE_API_DOCUMENTATION.md @@ -0,0 +1,411 @@ +# File Management API Documentation + +## Overview + +The File Management API provides a complete CRUD system for handling file uploads with organized storage, metadata management, and file organization by categories and clients. + +## Base URL + +``` +/api/files +``` + +## Authentication + +All file routes require authentication using Sanctum tokens. + +## File Organization Structure + +Files are automatically organized in storage following this structure: + +``` +storage/app/public/ +├── client/{client_id}/{category}/{subcategory}/filename.pdf +├── client/{client_id}/devis/DEVIS_2024_12_01_10_30_45.pdf +├── client/{client_id}/facture/FACT_2024_12_01_10_30_45.pdf +└── general/{category}/{subcategory}/filename.pdf +``` + +### Supported Categories + +- `devis` - Quotes/Devis +- `facture` - Invoices/Factures +- `contrat` - Contracts +- `document` - General Documents +- `image` - Images +- `autre` - Other files + +## Endpoints Overview + +### 1. List All Files + +```http +GET /api/files +``` + +**Parameters:** + +- `per_page` (optional): Number of files per page (default: 15) +- `search` (optional): Search by filename +- `mime_type` (optional): Filter by MIME type +- `uploaded_by` (optional): Filter by uploader user ID +- `category` (optional): Filter by category +- `client_id` (optional): Filter by client ID +- `date_from` (optional): Filter files uploaded after date (YYYY-MM-DD) +- `date_to` (optional): Filter files uploaded before date (YYYY-MM-DD) +- `sort_by` (optional): Sort field (default: uploaded_at) +- `sort_direction` (optional): Sort direction (default: desc) + +**Response:** + +```json +{ + "data": [ + { + "id": 1, + "file_name": "document.pdf", + "mime_type": "application/pdf", + "size_bytes": 1024000, + "size_formatted": "1000.00 KB", + "extension": "pdf", + "storage_uri": "client/123/devis/DEVIS_2024_12_01_10_30_45.pdf", + "organized_path": "client/123/devis/DEVIS_2024_12_01_10_30_45.pdf", + "sha256": "abc123...", + "uploaded_by": 1, + "uploader_name": "John Doe", + "uploaded_at": "2024-12-01 10:30:45", + "is_image": false, + "is_pdf": true, + "category": "devis", + "subcategory": "general" + } + ], + "pagination": { + "current_page": 1, + "from": 1, + "last_page": 5, + "per_page": 15, + "to": 15, + "total": 75, + "has_more_pages": true + }, + "summary": { + "total_files": 15, + "total_size": 15360000, + "total_size_formatted": "14.65 MB", + "categories": { + "devis": { + "count": 8, + "total_size_formatted": "8.24 MB" + }, + "facture": { + "count": 7, + "total_size_formatted": "6.41 MB" + } + } + } +} +``` + +### 2. Upload File + +```http +POST /api/files +``` + +**Content-Type:** `multipart/form-data` + +**Parameters:** + +- `file` (required): The file to upload (max 10MB) +- `file_name` (optional): Custom filename (uses original name if not provided) +- `category` (required): File category (devis|facture|contrat|document|image|autre) +- `client_id` (optional): Associated client ID +- `subcategory` (optional): Subcategory for better organization +- `description` (optional): File description (max 500 chars) +- `tags` (optional): Array of tags (max 10 tags, 50 chars each) +- `is_public` (optional): Whether file is publicly accessible + +**Example Request:** + +```bash +curl -X POST \ + http://localhost/api/files \ + -H "Authorization: Bearer {token}" \ + -F "file=@/path/to/document.pdf" \ + -F "category=devis" \ + -F "client_id=123" \ + -F "subcategory=annual" \ + -F "description=Annual quote for client" \ + -F 'tags[]=quote' \ + -F 'tags[]=annual' +``` + +**Success Response (201):** + +```json +{ + "data": { + "id": 1, + "file_name": "document.pdf", + "mime_type": "application/pdf", + "size_bytes": 1024000, + "storage_uri": "client/123/devis/annual/document_2024_12_01_10_30_45.pdf", + "category": "devis", + "subcategory": "annual", + "uploaded_at": "2024-12-01 10:30:45" + }, + "message": "Fichier téléchargé avec succès." +} +``` + +### 3. Get File Details + +```http +GET /api/files/{id} +``` + +**Response:** Same as file object in list endpoint + +### 4. Update File Metadata + +```http +PUT /api/files/{id} +``` + +**Parameters:** + +- `file_name` (optional): New filename +- `description` (optional): Updated description +- `tags` (optional): Updated tags array +- `is_public` (optional): Updated public status +- `category` (optional): Move to new category +- `client_id` (optional): Change associated client +- `subcategory` (optional): Update subcategory + +**Note:** If category, client_id, or subcategory are changed, the file will be automatically moved to the new location. + +### 5. Delete File + +```http +DELETE /api/files/{id} +``` + +**Response:** + +```json +{ + "message": "Fichier supprimé avec succès." +} +``` + +### 6. Get Files by Category + +```http +GET /api/files/by-category/{category} +``` + +**Parameters:** Same as list endpoint with additional `per_page` + +### 7. Get Files by Client + +```http +GET /api/files/by-client/{clientId} +``` + +**Parameters:** Same as list endpoint with additional `per_page` + +### 8. Get Organized File Structure + +```http +GET /api/files/organized +``` + +**Response:** + +```json +{ + "data": { + "devis/general": { + "category": "devis", + "subcategory": "general", + "files": [...], + "count": 25 + }, + "facture/annual": { + "category": "facture", + "subcategory": "annual", + "files": [...], + "count": 15 + } + }, + "message": "Structure de fichiers récupérée avec succès." +} +``` + +### 9. Get Storage Statistics + +```http +GET /api/files/statistics +``` + +**Response:** + +```json +{ + "data": { + "total_files": 150, + "total_size_bytes": 1073741824, + "total_size_formatted": "1.00 GB", + "by_type": { + "application/pdf": { + "count": 85, + "total_size": 734003200 + }, + "image/jpeg": { + "count": 45, + "total_size": 209715200 + } + }, + "by_category": { + "devis": { + "count": 60, + "total_size": 429496729 + }, + "facture": { + "count": 50, + "total_size": 322122547 + } + } + }, + "message": "Statistiques de stockage récupérées avec succès." +} +``` + +### 10. Generate Download URL + +```http +GET /api/files/{id}/download +``` + +**Response:** + +```json +{ + "data": { + "download_url": "/storage/client/123/devis/document_2024_12_01_10_30_45.pdf", + "file_name": "document.pdf", + "mime_type": "application/pdf" + }, + "message": "URL de téléchargement générée avec succès." +} +``` + +## Error Responses + +### Validation Error (422) + +```json +{ + "message": "Les données fournies ne sont pas valides.", + "errors": { + "file": ["Le fichier est obligatoire."], + "category": ["La catégorie est obligatoire."] + } +} +``` + +### Not Found (404) + +```json +{ + "message": "Fichier non trouvé." +} +``` + +### Server Error (500) + +```json +{ + "message": "Une erreur est survenue lors du traitement de la requête.", + "error": "Detailed error message (only in debug mode)" +} +``` + +## File Features + +### Automatic Organization + +- Files are automatically organized by category and client +- Timestamps are added to prevent filename conflicts +- Safe slug generation for subcategories + +### Security + +- SHA256 hash calculation for file integrity +- User-based access control +- File size validation (10MB limit) + +### Metadata Support + +- MIME type detection +- File size tracking +- Upload timestamp +- User attribution +- Custom tags and descriptions + +### Storage Management + +- Public storage disk usage +- Efficient path organization +- Duplicate prevention through hashing +- Automatic file moving on metadata updates + +## Usage Examples + +### Upload a Quote for Client + +```bash +curl -X POST \ + http://localhost/api/files \ + -H "Authorization: Bearer {token}" \ + -F "file=@quote_2024.pdf" \ + -F "category=devis" \ + -F "client_id=123" \ + -F "subcategory=annual_2024" \ + -F 'tags[]=quote' \ + -F 'tags[]=annual' +``` + +### Get All Client Files + +```bash +curl -X GET \ + "http://localhost/api/files/by-client/123?per_page=20&sort_by=uploaded_at&sort_direction=desc" \ + -H "Authorization: Bearer {token}" +``` + +### Get File Statistics + +```bash +curl -X GET \ + "http://localhost/api/files/statistics" \ + -H "Authorization: Bearer {token}" +``` + +### Search Files + +```bash +curl -X GET \ + "http://localhost/api/files?search=annual&category=devis" \ + -H "Authorization: Bearer {token}" +``` + +## Notes + +- All file operations are logged for audit purposes +- Files are stored in `storage/app/public/` directory +- The system automatically handles file moving when categories change +- Download URLs are generated on-demand for security +- Pagination is applied to all list endpoints +- French language is used for all API messages and validations diff --git a/thanasoft-back/IMPLEMENTATION_SUMMARY.md b/thanasoft-back/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..d49d659 --- /dev/null +++ b/thanasoft-back/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,219 @@ +# Intervention with All Data - Implementation Summary + +## Overview + +Created a comprehensive `createInterventionalldata` method in the InterventionController that handles creating an intervention along with all related entities (deceased, client, contact, location, documents) in a single atomic transaction. + +## Files Created/Modified + +### 1. Form Request + +**File**: `app/Http/Requests/StoreInterventionWithAllDataRequest.php` + +- Comprehensive validation for all entities +- Step-level error grouping (deceased, client, contact, location, documents, intervention) +- French error messages +- File validation for document uploads + +### 2. Controller Method + +**File**: `app/Http/Controllers/Api/InterventionController.php` + +- Added `createInterventionalldata()` method +- Database transaction wrapping all operations +- Step-by-step creation flow: + 1. Create deceased + 2. Create client + 3. Create contact (if provided) + 4. Prepare location data + 5. Create intervention + 6. Handle document uploads +- Automatic rollback on any error +- Comprehensive logging + +### 3. API Route + +**File**: `routes/api.php` + +- Added endpoint: `POST /api/interventions/with-all-data` +- Protected by authentication middleware + +### 4. Repository Improvements + +**Files**: + +- `app/Repositories/DeceasedRepositoryInterface.php` (created) +- `app/Repositories/DeceasedRepository.php` (updated) + +**Changes**: + +- DeceasedRepository now extends BaseRepository +- Implements BaseRepositoryInterface +- Inherits transaction support from BaseRepository +- Consistent with other repository implementations + +## API Endpoint + +### POST /api/interventions/with-all-data + +#### Request Structure: + +```json +{ + "deceased": { + "last_name": "Required", + "first_name": "Optional", + "birth_date": "Optional", + "death_date": "Optional", + "place_of_death": "Optional", + "notes": "Optional" + }, + "client": { + "name": "Required", + "vat_number": "Optional", + "siret": "Optional", + "email": "Optional", + "phone": "Optional", + "billing_address_line1": "Optional", + "billing_postal_code": "Optional", + "billing_city": "Optional", + "billing_country_code": "Optional", + "notes": "Optional", + "is_active": "Optional" + }, + "contact": { + "first_name": "Optional", + "last_name": "Optional", + "email": "Optional", + "phone": "Optional", + "role": "Optional" + }, + "location": { + "name": "Optional", + "address": "Optional", + "city": "Optional", + "postal_code": "Optional", + "country_code": "Optional", + "access_instructions": "Optional", + "notes": "Optional" + }, + "documents": [ + { + "file": "File upload", + "name": "Required", + "description": "Optional" + } + ], + "intervention": { + "type": "Required", + "scheduled_at": "Optional", + "duration_min": "Optional", + "status": "Optional", + "assigned_practitioner_id": "Optional", + "order_giver": "Optional", + "notes": "Optional", + "created_by": "Optional" + } +} +``` + +#### Success Response (201): + +```json +{ + "message": "Intervention créée avec succès", + "data": { + "intervention": { ... }, + "deceased": { ... }, + "client": { ... }, + "contact_id": 123, + "documents_count": 2 + } +} +``` + +#### Error Response (422 - Validation): + +```json +{ + "message": "Données invalides", + "errors": { + "deceased": ["Le nom de famille est obligatoire."], + "client": ["Le nom du client est obligatoire."], + "intervention": ["Le type d'intervention est obligatoire."] + } +} +``` + +## Transaction Flow + +``` +DB::beginTransaction() + 1. Create Deceased + 2. Create Client + 3. Create Contact (if provided) + 4. Prepare Location Notes + 5. Create Intervention + 6. Handle Document Uploads (pending implementation) +DB::commit() +``` + +## Error Handling + +### Validation Errors + +- Caught separately with proper HTTP status (422) +- Grouped by step for better UX +- French error messages + +### General Errors + +- Caught with proper HTTP status (500) +- Full exception logged with trace +- Input data logged (excluding files) +- Transaction automatically rolled back + +## Data Integrity + +1. **BaseRepository Transaction Support**: All repository create operations use transactions +2. **Controller-level Transaction**: Main transaction wraps all operations +3. **Automatic Rollback**: Any exception triggers automatic rollback +4. **Validation Before Transaction**: All data validated before any DB operations + +## Benefits + +1. **Atomicity**: All-or-nothing operation - prevents partial data creation +2. **Data Integrity**: No orphaned records if any step fails +3. **Performance**: Single HTTP request for complex operation +4. **User Experience**: One form instead of multiple steps +5. **Validation**: Comprehensive client and server-side validation +6. **Logging**: Full audit trail for debugging + +## Next Steps + +1. **Document Upload Implementation**: Complete file upload and storage logic +2. **Location Model**: Consider creating a proper Location model instead of notes +3. **Client Location Association**: Link interventions to actual locations +4. **File Storage**: Implement proper file storage with intervention folders +5. **Email Notifications**: Add notifications to relevant parties +6. **Audit Trail**: Add more detailed logging for compliance + +## Testing + +To test the endpoint: + +```bash +# Create intervention with all data +curl -X POST http://localhost/api/interventions/with-all-data \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d @intervention-data.json +``` + +## Notes + +- All date fields should use ISO format (Y-m-d H:i:s) +- Document files should be sent as multipart/form-data +- Location data is currently appended to intervention notes (can be enhanced) +- Contact creation is optional - only created if data provided +- Default status is "demande" if not specified diff --git a/thanasoft-back/app/Http/Controllers/Api/AccessControlController.php b/thanasoft-back/app/Http/Controllers/Api/AccessControlController.php new file mode 100644 index 0000000..33de48b --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/AccessControlController.php @@ -0,0 +1,252 @@ +json([ + 'data' => $this->accessControlRepository->index(), + 'message' => 'Roles et permissions recuperes avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error fetching access control data: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recuperation des roles et permissions.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function storeRole(Request $request): JsonResponse + { + $validated = $request->validate([ + 'name' => ['required', 'string', 'max:100', 'unique:roles,name'], + 'guard_name' => ['nullable', 'string', 'max:50'], + 'permissions' => ['nullable', 'array'], + 'permissions.*' => ['string', 'max:150'], + ]); + + try { + $role = $this->accessControlRepository->createRole($validated); + + return response()->json([ + 'data' => $role, + 'message' => 'Role cree avec succes.', + ], 201); + } catch (\Exception $e) { + Log::error('Error creating role: ' . $e->getMessage(), [ + 'exception' => $e, + 'data' => $validated, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la creation du role.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function updateRole(Request $request, string $id): JsonResponse + { + $validated = $request->validate([ + 'name' => ['sometimes', 'string', 'max:100', 'unique:roles,name,' . $id], + 'guard_name' => ['nullable', 'string', 'max:50'], + 'permissions' => ['nullable', 'array'], + 'permissions.*' => ['string', 'max:150'], + ]); + + try { + $role = $this->accessControlRepository->updateRole((int) $id, $validated); + + if (! $role) { + return response()->json([ + 'message' => 'Role non trouve.', + ], 404); + } + + return response()->json([ + 'data' => $role, + 'message' => 'Role mis a jour avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error updating role: ' . $e->getMessage(), [ + 'exception' => $e, + 'role_id' => $id, + 'data' => $validated, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise a jour du role.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function destroyRole(string $id): JsonResponse + { + try { + $deleted = $this->accessControlRepository->deleteRole((int) $id); + + if (! $deleted) { + return response()->json([ + 'message' => 'Role non trouve.', + ], 404); + } + + return response()->json([ + 'message' => 'Role supprime avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error deleting role: ' . $e->getMessage(), [ + 'exception' => $e, + 'role_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du role.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function syncRolePermissions(Request $request, string $id): JsonResponse + { + $validated = $request->validate([ + 'permissions' => ['required', 'array'], + 'permissions.*' => ['string', 'max:150'], + ]); + + try { + $role = $this->accessControlRepository->syncRolePermissions((int) $id, $validated['permissions']); + + if (! $role) { + return response()->json([ + 'message' => 'Role non trouve.', + ], 404); + } + + return response()->json([ + 'data' => $role, + 'message' => 'Permissions du role synchronisees avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error syncing role permissions: ' . $e->getMessage(), [ + 'exception' => $e, + 'role_id' => $id, + 'data' => $validated, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la synchronisation des permissions du role.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function storePermission(Request $request): JsonResponse + { + $validated = $request->validate([ + 'name' => ['required', 'string', 'max:150', 'unique:permissions,name'], + 'guard_name' => ['nullable', 'string', 'max:50'], + ]); + + try { + $permission = $this->accessControlRepository->createPermission($validated); + + return response()->json([ + 'data' => $permission, + 'message' => 'Permission creee avec succes.', + ], 201); + } catch (\Exception $e) { + Log::error('Error creating permission: ' . $e->getMessage(), [ + 'exception' => $e, + 'data' => $validated, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la creation de la permission.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function updatePermission(Request $request, string $id): JsonResponse + { + $validated = $request->validate([ + 'name' => ['sometimes', 'string', 'max:150', 'unique:permissions,name,' . $id], + 'guard_name' => ['nullable', 'string', 'max:50'], + ]); + + try { + $permission = $this->accessControlRepository->updatePermission((int) $id, $validated); + + if (! $permission) { + return response()->json([ + 'message' => 'Permission non trouvee.', + ], 404); + } + + return response()->json([ + 'data' => $permission, + 'message' => 'Permission mise a jour avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error updating permission: ' . $e->getMessage(), [ + 'exception' => $e, + 'permission_id' => $id, + 'data' => $validated, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise a jour de la permission.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function destroyPermission(string $id): JsonResponse + { + try { + $deleted = $this->accessControlRepository->deletePermission((int) $id); + + if (! $deleted) { + return response()->json([ + 'message' => 'Permission non trouvee.', + ], 404); + } + + return response()->json([ + 'message' => 'Permission supprimee avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error deleting permission: ' . $e->getMessage(), [ + 'exception' => $e, + 'permission_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression de la permission.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Http/Controllers/Api/AuthController.php b/thanasoft-back/app/Http/Controllers/Api/AuthController.php index 9fd413b..5d9c517 100644 --- a/thanasoft-back/app/Http/Controllers/Api/AuthController.php +++ b/thanasoft-back/app/Http/Controllers/Api/AuthController.php @@ -19,6 +19,10 @@ class AuthController extends BaseController $data = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'], + 'roles' => ['nullable', 'array'], + 'roles.*' => ['string', 'max:100'], + 'permissions' => ['nullable', 'array'], + 'permissions.*' => ['string', 'max:150'], 'password' => ['required', Password::min(8)], ]); @@ -29,10 +33,18 @@ class AuthController extends BaseController 'password' => $data['password'], // hashed via User model cast ]); + if (! empty($data['roles'])) { + $user->syncRoles($data['roles']); + } + + if (! empty($data['permissions'])) { + $user->syncPermissions($data['permissions']); + } + $token = $user->createToken('api')->plainTextToken; return $this->sendResponse([ - 'user' => $user, + 'user' => $user->load('roles', 'permissions'), 'token' => $token, ], 'User registered successfully.'); @@ -61,7 +73,7 @@ class AuthController extends BaseController $token = $user->createToken('api')->plainTextToken; return $this->sendResponse([ - 'user' => $user, + 'user' => $user->load('roles', 'permissions'), 'token' => $token, ], 'Login successful.'); @@ -72,6 +84,75 @@ class AuthController extends BaseController } } + public function checkEmail(Request $request): JsonResponse + { + try { + $data = $request->validate([ + 'email' => ['required', 'email'], + ]); + + $user = User::where('email', $data['email'])->first(); + + if (! $user) { + return $this->sendError('Utilisateur introuvable.', [ + 'email' => ['Aucun utilisateur ne correspond a cet email.'], + ], 404); + } + + return $this->sendResponse([ + 'user' => [ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + ], + 'has_password' => ! empty($user->getRawOriginal('password')), + ], 'Email verifie avec succes.'); + } catch (ValidationException $e) { + return $this->sendError('Validation Error.', $e->errors(), 422); + } catch (\Exception $e) { + return $this->sendError('Email check failed.', ['error' => $e->getMessage()], 500); + } + } + + public function createPasswordAndLogin(Request $request): JsonResponse + { + try { + $data = $request->validate([ + 'email' => ['required', 'email'], + 'password' => ['required', 'confirmed', Password::min(8)], + ]); + + /** @var User|null $user */ + $user = User::where('email', $data['email'])->first(); + + if (! $user) { + return $this->sendError('Utilisateur introuvable.', [ + 'email' => ['Aucun utilisateur ne correspond a cet email.'], + ], 404); + } + + if (! empty($user->getRawOriginal('password'))) { + return $this->sendError('Mot de passe deja defini.', [ + 'password' => ['Cet utilisateur a deja un mot de passe.'], + ], 422); + } + + $user->password = $data['password']; + $user->save(); + + $token = $user->createToken('api')->plainTextToken; + + return $this->sendResponse([ + 'user' => $user->load('roles', 'permissions'), + 'token' => $token, + ], 'Mot de passe cree et connexion reussie.'); + } catch (ValidationException $e) { + return $this->sendError('Validation Error.', $e->errors(), 422); + } catch (\Exception $e) { + return $this->sendError('Password creation failed.', ['error' => $e->getMessage()], 500); + } + } + public function me(Request $request): JsonResponse { try { @@ -81,7 +162,7 @@ class AuthController extends BaseController return $this->sendError('Unauthenticated.', [], 401); } - return $this->sendResponse($user, 'User retrieved successfully.'); + return $this->sendResponse($user->load('roles', 'permissions'), 'User retrieved successfully.'); } catch (\Exception $e) { return $this->sendError('Failed to retrieve user.', ['error' => $e->getMessage()], 500); diff --git a/thanasoft-back/app/Http/Controllers/Api/AvoirController.php b/thanasoft-back/app/Http/Controllers/Api/AvoirController.php new file mode 100644 index 0000000..8bf3591 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/AvoirController.php @@ -0,0 +1,151 @@ +avoirRepository->all(); + return AvoirResource::collection($avoirs); + } catch (\Exception $e) { + Log::error('Error fetching avoirs: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des avoirs.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created credit. + */ + public function store(StoreAvoirRequest $request): AvoirResource|JsonResponse + { + try { + $avoir = $this->avoirRepository->create($request->validated()); + return new AvoirResource($avoir); + } catch (\Exception $e) { + Log::error('Error creating avoir: ' . $e->getMessage(), [ + 'exception' => $e, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création de l\'avoir.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified credit. + */ + public function show(string $id): AvoirResource|JsonResponse + { + try { + $avoir = $this->avoirRepository->find($id); + + if (!$avoir) { + return response()->json([ + 'message' => 'Avoir non trouvé.', + ], 404); + } + + return new AvoirResource($avoir); + } catch (\Exception $e) { + Log::error('Error fetching avoir: ' . $e->getMessage(), [ + 'exception' => $e, + 'avoir_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération de l\'avoir.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified credit. + */ + public function update(UpdateAvoirRequest $request, string $id): AvoirResource|JsonResponse + { + try { + $updated = $this->avoirRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Avoir non trouvé ou échec de la mise à jour.', + ], 404); + } + + $avoir = $this->avoirRepository->find($id); + return new AvoirResource($avoir); + } catch (\Exception $e) { + Log::error('Error updating avoir: ' . $e->getMessage(), [ + 'exception' => $e, + 'avoir_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour de l\'avoir.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified credit. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->avoirRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Avoir non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Avoir supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting avoir: ' . $e->getMessage(), [ + 'exception' => $e, + 'avoir_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression de l\'avoir.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/ClientActivityTimelineController.php b/thanasoft-back/app/Http/Controllers/Api/ClientActivityTimelineController.php new file mode 100644 index 0000000..48fa25c --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/ClientActivityTimelineController.php @@ -0,0 +1,40 @@ +repository = $repository; + } + + /** + * Get activity timeline for a client + */ + public function index(Request $request, Client $client) + { + try { + $perPage = (int) $request->get('per_page', 10); + + $activities = $this->repository->getByClient($client->id, $perPage); + + return ClientActivityTimelineResource::collection($activities); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error fetching client timeline: ' . $e->getMessage() + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/ClientCategoryController.php b/thanasoft-back/app/Http/Controllers/Api/ClientCategoryController.php new file mode 100644 index 0000000..22b8cd8 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/ClientCategoryController.php @@ -0,0 +1,153 @@ +get(); + + return ClientCategoryResource::collection($categories); + } + + /** + * Store a newly created resource in storage. + */ + public function store(ClientCategoryRequest $request): ClientCategoryResource + { + $category = ClientCategory::create($request->validated()); + + return new ClientCategoryResource($category); + } + + /** + * Display the specified resource. + */ + public function show(ClientCategory $clientCategory): ClientCategoryResource + { + return new ClientCategoryResource($clientCategory); + } + + /** + * Display the specified resource by slug. + */ + public function showBySlug(string $slug): ClientCategoryResource + { + $category = ClientCategory::where('slug', $slug)->firstOrFail(); + + return new ClientCategoryResource($category); + } + + /** + * Update the specified resource in storage. + */ + public function update(ClientCategoryRequest $request, ClientCategory $clientCategory): ClientCategoryResource + { + $clientCategory->update($request->validated()); + + return new ClientCategoryResource($clientCategory->fresh()); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(ClientCategory $clientCategory): JsonResponse + { + // Check if category has clients + if ($clientCategory->clients()->exists()) { + return response()->json([ + 'success' => false, + 'message' => 'Cannot delete category that has clients assigned. Please reassign clients first.' + ], 422); + } + + $clientCategory->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Client category deleted successfully.' + ]); + } + + /** + * Toggle active status of the category. + */ + public function toggleStatus(ClientCategory $clientCategory, Request $request): ClientCategoryResource + { + $request->validate([ + 'is_active' => 'required|boolean', + ]); + + $clientCategory->update([ + 'is_active' => $request->boolean('is_active'), + ]); + + return new ClientCategoryResource($clientCategory->fresh()); + } + + /** + * Get clients for a specific category. + */ + public function clients(ClientCategory $clientCategory, Request $request): AnonymousResourceCollection + { + $query = $clientCategory->clients(); + + // Active status filter + if ($request->has('is_active')) { + $query->where('is_active', $request->boolean('is_active')); + } + + // Pagination + $perPage = $request->get('per_page', 15); + $clients = $query->paginate($perPage); + + return ClientResource::collection($clients); + } + + /** + * Reorder categories. + */ + public function reorder(Request $request): JsonResponse + { + $request->validate([ + 'order' => 'required|array', + 'order.*' => 'integer|exists:client_categories,id', + ]); + + foreach ($request->order as $index => $categoryId) { + ClientCategory::where('id', $categoryId)->update(['sort_order' => $index]); + } + + return response()->json([ + 'success' => true, + 'message' => 'Categories reordered successfully.' + ]); + } + + /** + * Get all active categories for dropdowns. + */ + public function active(): AnonymousResourceCollection + { + $categories = ClientCategory::where('is_active', true) + ->orderBy('sort_order', 'asc') + ->orderBy('name', 'asc') + ->get(); + + return ClientCategoryResource::collection($categories); + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/ClientController.php b/thanasoft-back/app/Http/Controllers/Api/ClientController.php new file mode 100644 index 0000000..8b53f2c --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/ClientController.php @@ -0,0 +1,339 @@ +get('per_page', 15); + $filters = [ + 'search' => $request->get('search'), + 'is_active' => $request->get('is_active'), + 'group_id' => $request->get('group_id'), + 'client_category_id' => $request->get('client_category_id'), + 'sort_by' => $request->get('sort_by', 'created_at'), + 'sort_direction' => $request->get('sort_direction', 'desc'), + ]; + + // Remove null filters + $filters = array_filter($filters, function ($value) { + return $value !== null && $value !== ''; + }); + + $clients = $this->clientRepository->paginate($perPage, $filters); + + return new ClientCollection($clients); + + } catch (\Exception $e) { + Log::error('Error fetching clients: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des clients.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created client. + */ + public function store(StoreClientRequest $request): ClientResource|JsonResponse + { + try { + $client = $this->clientRepository->create($request->validated()); + return new ClientResource($client); + } catch (\Exception $e) { + Log::error('Error creating client: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified client. + */ + public function show(string $id): ClientResource|JsonResponse + { + try { + $client = $this->clientRepository->find($id); + + if (!$client) { + return response()->json([ + 'message' => 'Client non trouvé.', + ], 404); + } + + return new ClientResource($client); + } catch (\Exception $e) { + Log::error('Error fetching client: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function searchBy(Request $request): JsonResponse + { + try { + $name = $request->get('name', ''); + + if (empty($name)) { + return response()->json([ + 'message' => 'Le paramètre "name" est requis.', + ], 400); + } + + $clients = $this->clientRepository->searchByName($name); + + return response()->json([ + 'data' => $clients, + 'count' => $clients->count(), + 'message' => $clients->count() > 0 + ? 'Clients trouvés avec succès.' + : 'Aucun client trouvé.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error searching clients by name: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'search_term' => $name, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recherche des clients.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified client. + */ + public function update(UpdateClientRequest $request, string $id): ClientResource|JsonResponse + { + try { + $validatedData = $request->validated(); + + $client = $this->clientRepository->find($id); + if (!$client) { + return response()->json([ + 'message' => 'Client non trouvé.', + ], 404); + } + + if ($request->hasFile('avatar')) { + // Delete old avatar if exists + if ($client->avatar) { + Storage::disk('public')->delete($client->avatar); + } + + // Store new avatar + $path = $request->file('avatar')->store('avatars', 'public'); + $validatedData['avatar'] = $path; + } + + $updated = $this->clientRepository->update($id, $validatedData); + + if (!$updated) { + return response()->json([ + 'message' => 'Échec de la mise à jour.', + ], 500); + } + + $client = $this->clientRepository->find($id); + return new ClientResource($client); + } catch (\Exception $e) { + Log::error('Error updating client: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified client. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->clientRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Client non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Client supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting client: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + /** + * Change client status (active/inactive). + */ + public function changeStatus(Request $request, string $id): ClientResource|JsonResponse + { + try { + $isActive = $request->input('is_active'); + $updated = $this->clientRepository->update($id, ['is_active' => $isActive]); + + if (!$updated) { + return response()->json([ + 'message' => 'Client non trouvé ou échec de la mise à jour.', + ], 404); + } + + $client = $this->clientRepository->find($id); + return new ClientResource($client); + } catch (\Exception $e) { + Log::error('Error changing client status: ' . $e->getMessage(), [ + 'exception' => $e, + 'client_id' => $id, + ]); + return response()->json(['message' => 'Erreur serveur'], 500); + } + } + + /** + * Get children clients. + */ + public function getChildren(string $id): AnonymousResourceCollection|JsonResponse + { + try { + $client = $this->clientRepository->find($id); + if (!$client) { + return response()->json(['message' => 'Client not found'], 404); + } + + // Assuming the relationship is defined in the model as 'children' + $children = $client->children; + return ClientResource::collection($children); + + } catch (\Exception $e) { + Log::error('Error fetching children: ' . $e->getMessage(), [ + 'exception' => $e, + 'client_id' => $id, + ]); + return response()->json(['message' => 'Erreur serveur'], 500); + } + } + + /** + * Add a child client. + */ + public function addChild(string $id, string $childId): JsonResponse + { + try { + $parent = $this->clientRepository->find($id); + $child = $this->clientRepository->find($childId); + + if (!$parent || !$child) { + return response()->json(['message' => 'Parent or Child not found'], 404); + } + + // Update child's parent_id + $this->clientRepository->update($childId, ['parent_id' => $id]); + + return response()->json(['message' => 'Child added successfully'], 200); + + } catch (\Exception $e) { + Log::error('Error adding child: ' . $e->getMessage(), [ + 'exception' => $e, + 'parent_id' => $id, + 'child_id' => $childId + ]); + return response()->json(['message' => 'Erreur serveur'], 500); + } + } + + /** + * Remove a child client. + */ + public function removeChild(string $id, string $childId): JsonResponse + { + try { + $child = $this->clientRepository->find($childId); + + if (!$child) { + return response()->json(['message' => 'Child not found'], 404); + } + + if ($child->parent_id != $id) { + return response()->json(['message' => 'Client is not a child of this parent'], 400); + } + + // Remove parent_id + $this->clientRepository->update($childId, ['parent_id' => null]); + + return response()->json(['message' => 'Child removed successfully'], 200); + + } catch (\Exception $e) { + Log::error('Error removing child: ' . $e->getMessage(), [ + 'exception' => $e, + 'parent_id' => $id, + 'child_id' => $childId + ]); + return response()->json(['message' => 'Erreur serveur'], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/ClientGroupController.php b/thanasoft-back/app/Http/Controllers/Api/ClientGroupController.php new file mode 100644 index 0000000..c60eb29 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/ClientGroupController.php @@ -0,0 +1,257 @@ +get('per_page', 5); + $filters = array_filter([ + 'search' => $request->get('search'), + 'sort_by' => $request->get('sort_by', 'created_at'), + 'sort_direction' => $request->get('sort_direction', 'desc'), + ], static fn ($value) => $value !== null && $value !== ''); + + $clientGroups = $this->clientGroupRepository->paginate($perPage, $filters); + + return ClientGroupResource::collection($clientGroups); + } catch (\Exception $e) { + Log::error('Error fetching client groups: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des groupes de clients.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created client group. + */ + public function store(StoreClientGroupRequest $request): ClientGroupResource|JsonResponse + { + try { + $validated = $request->validated(); + + $clientGroup = DB::transaction(function () use ($validated) { + $clientIds = Arr::get($validated, 'client_ids', []); + + $clientGroup = $this->clientGroupRepository->create(Arr::except($validated, ['client_ids'])); + + if (!empty($clientIds)) { + Client::query() + ->whereIn('id', $clientIds) + ->update(['group_id' => $clientGroup->id]); + } + + return $clientGroup->load(['clients'])->loadCount('clients'); + }); + + return new ClientGroupResource($clientGroup); + } catch (\Exception $e) { + Log::error('Error creating client group: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du groupe de clients.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified client group. + */ + public function show(string $id): ClientGroupResource|JsonResponse + { + try { + $clientGroup = $this->clientGroupRepository->find($id); + + if (!$clientGroup) { + return response()->json([ + 'message' => 'Groupe de clients non trouvé.', + ], 404); + } + + $clientGroup->load(['clients'])->loadCount('clients'); + + return new ClientGroupResource($clientGroup); + } catch (\Exception $e) { + Log::error('Error fetching client group: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_group_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du groupe de clients.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified client group. + */ + public function update(UpdateClientGroupRequest $request, string $id): ClientGroupResource|JsonResponse + { + try { + $validated = $request->validated(); + + $updated = DB::transaction(function () use ($id, $validated) { + $updated = $this->clientGroupRepository->update($id, Arr::except($validated, ['client_ids'])); + + if (!$updated) { + return false; + } + + if (array_key_exists('client_ids', $validated)) { + $clientIds = $validated['client_ids'] ?? []; + + Client::query() + ->where('group_id', (int) $id) + ->whereNotIn('id', $clientIds) + ->update(['group_id' => null]); + + if (!empty($clientIds)) { + Client::query() + ->whereIn('id', $clientIds) + ->update(['group_id' => (int) $id]); + } + } + + return true; + }); + + if (!$updated) { + return response()->json([ + 'message' => 'Groupe de clients non trouvé ou échec de la mise à jour.', + ], 404); + } + + $clientGroup = $this->clientGroupRepository->find($id); + $clientGroup?->load(['clients'])->loadCount('clients'); + + return new ClientGroupResource($clientGroup); + } catch (\Exception $e) { + Log::error('Error updating client group: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_group_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du groupe de clients.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified client group. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->clientGroupRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Groupe de clients non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Groupe de clients supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting client group: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_group_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du groupe de clients.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Assign many clients to one client group. + */ + public function assignClients(AssignClientsToGroupRequest $request, string $id): JsonResponse + { + try { + $clientGroup = $this->clientGroupRepository->find($id); + + if (!$clientGroup) { + return response()->json([ + 'message' => 'Groupe de clients non trouvé.', + ], 404); + } + + $clientIds = $request->validated('client_ids'); + + $updatedCount = DB::transaction(function () use ($clientIds, $clientGroup) { + return Client::query() + ->whereIn('id', $clientIds) + ->update(['group_id' => $clientGroup->id]); + }); + + $clientGroup->load(['clients'])->loadCount('clients'); + + return response()->json([ + 'message' => 'Clients assignés au groupe avec succès.', + 'assigned_count' => $updatedCount, + 'group' => new ClientGroupResource($clientGroup), + ]); + } catch (\Exception $e) { + Log::error('Error assigning clients to group: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_group_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de l’assignation des clients au groupe.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/ClientLocationController.php b/thanasoft-back/app/Http/Controllers/Api/ClientLocationController.php new file mode 100644 index 0000000..6021150 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/ClientLocationController.php @@ -0,0 +1,183 @@ +only(['client_id', 'is_default', 'search']); + $perPage = (int) $request->get('per_page', 10); + + $clientLocations = $this->clientLocationRepository->getPaginated($filters, $perPage); + return new ClientLocationCollection($clientLocations); + } catch (\Exception $e) { + Log::error('Error fetching client locations: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des lieux clients.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created client location. + */ + public function store(StoreClientLocationRequest $request): ClientLocationResource|JsonResponse + { + try { + $clientLocation = $this->clientLocationRepository->create($request->validated()); + return new ClientLocationResource($clientLocation); + } catch (\Exception $e) { + Log::error('Error creating client location: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du lieu client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified client id. + */ + public function getLocationsByClient(string $id) + { + try { + $clientLocations = $this->clientLocationRepository->getByClientId((int)$id); + return ClientLocationResource::collection($clientLocations); + } catch (\Exception $e) { + Log::error('Error fetching client location: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_location_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du lieu client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified client location. + */ + public function show(string $id): ClientLocationResource|JsonResponse + { + try { + $clientLocation = $this->clientLocationRepository->find($id); + + if (!$clientLocation) { + return response()->json([ + 'message' => 'Lieu client non trouvé.', + ], 404); + } + + return new ClientLocationResource($clientLocation); + } catch (\Exception $e) { + Log::error('Error fetching client location: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_location_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du lieu client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified client location. + */ + public function update(UpdateClientLocationRequest $request, string $id): ClientLocationResource|JsonResponse + { + try { + $updated = $this->clientLocationRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Lieu client non trouvé ou échec de la mise à jour.', + ], 404); + } + + $clientLocation = $this->clientLocationRepository->find($id); + return new ClientLocationResource($clientLocation); + } catch (\Exception $e) { + Log::error('Error updating client location: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_location_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du lieu client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified client location. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->clientLocationRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Lieu client non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Lieu client supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting client location: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_location_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du lieu client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/ContactController.php b/thanasoft-back/app/Http/Controllers/Api/ContactController.php new file mode 100644 index 0000000..493962b --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/ContactController.php @@ -0,0 +1,214 @@ + request('search'), + 'is_primary' => request('is_primary'), + 'client_id' => request('client_id'), + 'sort_by' => request('sort_by', 'created_at'), + 'sort_direction' => request('sort_direction', 'desc'), + ]; + + $contacts = $this->contactRepository->paginate($perPage, $filters); + + return new ContactCollection($contacts); + } catch (\Exception $e) { + Log::error('Error fetching contacts: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des contacts.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created contact. + */ + public function store(StoreContactRequest $request): ContactResource|JsonResponse + { + try { + $contact = $this->contactRepository->create($request->validated()); + return new ContactResource($contact); + } catch (\Exception $e) { + Log::error('Error creating contact: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du contact.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified contact. + */ + public function show(string $id): ContactResource|JsonResponse + { + try { + $contact = $this->contactRepository->find($id); + + if (!$contact) { + return response()->json([ + 'message' => 'Contact non trouvé.', + ], 404); + } + + return new ContactResource($contact); + } catch (\Exception $e) { + Log::error('Error fetching contact: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'contact_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du contact.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified contact. + */ + public function update(UpdateContactRequest $request, string $id): ContactResource|JsonResponse + { + try { + $updated = $this->contactRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Contact non trouvé ou échec de la mise à jour.', + ], 404); + } + + $contact = $this->contactRepository->find($id); + return new ContactResource($contact); + } catch (\Exception $e) { + Log::error('Error updating contact: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'contact_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du contact.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified contact. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->contactRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Contact non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Contact supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting contact: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'contact_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du contact.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + + public function getContactsByClient(string $clientId): JsonResponse + { + try { + $intId = (int) $clientId; + $contacts = $this->contactRepository->getByClientId($intId); + return response()->json([ + 'data' => ContactResource::collection($contacts), + ], 200); + } catch (\Exception $e) { + Log::error('Error fetching contacts by client: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_id' => $clientId, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des contacts du client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + + + public function getContactsByFournisseur(string $fournisseurId): JsonResponse + { + try { + $intId = (int) $fournisseurId; + $contacts = $this->contactRepository->getByFournisseurId($intId); + return response()->json([ + 'data' => ContactResource::collection($contacts), + ], 200); + } catch (\Exception $e) { + Log::error('Error fetching contacts by fournisseur: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'fournisseur_id' => $fournisseurId, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des contacts du fournisseur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/ConvoyController.php b/thanasoft-back/app/Http/Controllers/Api/ConvoyController.php new file mode 100644 index 0000000..20eb42a --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/ConvoyController.php @@ -0,0 +1,144 @@ +convoyRepository->paginate( + (int) $request->integer('per_page', 15), + $request->only(['search', 'status', 'convoy_type', 'vehicle_id', 'deceased_id', 'sort_by', 'sort_direction']) + ); + + return response()->json([ + 'data' => ConvoyResource::collection($convoys->items()), + 'meta' => [ + 'current_page' => $convoys->currentPage(), + 'last_page' => $convoys->lastPage(), + 'per_page' => $convoys->perPage(), + 'total' => $convoys->total(), + ], + 'status' => 'success', + ]); + } catch (\Exception $e) { + Log::error('Error fetching convoys: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while fetching convoys.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function store(StoreConvoyRequest $request): JsonResponse + { + try { + $convoy = $this->convoyRepository->create($request->validated()); + + return response()->json([ + 'data' => new ConvoyResource($convoy->load(['deceased', 'client', 'vehicle', 'departureLocation'])), + 'message' => 'Convoy created successfully.', + 'status' => 'success', + ], 201); + } catch (\Exception $e) { + Log::error('Error creating convoy: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while creating the convoy.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function show(string $id): JsonResponse + { + try { + $convoy = $this->convoyRepository->find((int) $id); + + if (! $convoy) { + return response()->json(['message' => 'Convoy not found.'], 404); + } + + $convoy->load(['deceased', 'client', 'vehicle', 'departureLocation']); + + return response()->json([ + 'data' => new ConvoyResource($convoy), + 'status' => 'success', + ]); + } catch (\Exception $e) { + Log::error('Error fetching convoy: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while fetching the convoy.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function update(UpdateConvoyRequest $request, string $id): JsonResponse + { + try { + $updated = $this->convoyRepository->update((int) $id, $request->validated()); + + if (! $updated) { + return response()->json(['message' => 'Convoy not found or update failed.'], 404); + } + + $convoy = $this->convoyRepository->find((int) $id); + + return response()->json([ + 'data' => new ConvoyResource($convoy->load(['deceased', 'client', 'vehicle', 'departureLocation'])), + 'message' => 'Convoy updated successfully.', + 'status' => 'success', + ]); + } catch (\Exception $e) { + Log::error('Error updating convoy: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while updating the convoy.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->convoyRepository->delete((int) $id); + + if (! $deleted) { + return response()->json(['message' => 'Convoy not found or delete failed.'], 404); + } + + return response()->json([ + 'message' => 'Convoy deleted successfully.', + 'status' => 'success', + ]); + } catch (\Exception $e) { + Log::error('Error deleting convoy: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while deleting the convoy.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/DeceasedController.php b/thanasoft-back/app/Http/Controllers/Api/DeceasedController.php new file mode 100644 index 0000000..e3f25b4 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/DeceasedController.php @@ -0,0 +1,183 @@ +deceasedRepository = $deceasedRepository; + } + + /** + * Display a listing of the resource. + */ + public function index(Request $request): JsonResponse + { + try { + $filters = $request->only([ + 'search', + 'start_date', + 'end_date', + 'sort_by', + 'sort_order' + ]); + + $perPage = $request->input('per_page', 15); + + $deceased = $this->deceasedRepository->getAllPaginated($filters, $perPage); + + return response()->json(new DeceasedCollection($deceased)); + } catch (\Exception $e) { + Log::error('Error fetching deceased list: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des défunts.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Store a newly created resource in storage. + */ + public function store(StoreDeceasedRequest $request): JsonResponse + { + try { + $validated = $request->validated(); + + $deceased = $this->deceasedRepository->create($validated); + + return response()->json(new DeceasedResource($deceased), Response::HTTP_CREATED); + } catch (\Exception $e) { + Log::error('Error creating deceased: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du défunt.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Display the specified resource. + */ + public function show(int $id): JsonResponse + { + try { + $deceased = $this->deceasedRepository->findById($id); + + return response()->json(new DeceasedResource($deceased)); + } catch (\Exception $e) { + Log::error('Error fetching deceased details: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Défunt non trouvé ou une erreur est survenue.', + 'error' => $e->getMessage() + ], Response::HTTP_NOT_FOUND); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(UpdateDeceasedRequest $request, int $id): JsonResponse + { + try { + $deceased = $this->deceasedRepository->findById($id); + + $validated = $request->validated(); + + $updatedDeceased = $this->deceasedRepository->update($deceased, $validated); + + return response()->json(new DeceasedResource($updatedDeceased)); + } catch (\Exception $e) { + Log::error('Error updating deceased: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du défunt.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(int $id): JsonResponse + { + try { + $deceased = $this->deceasedRepository->findById($id); + + $this->deceasedRepository->delete($deceased); + + return response()->json(null, Response::HTTP_NO_CONTENT); + } catch (\Exception $e) { + Log::error('Error deleting deceased: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du défunt.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + /** + * Search deceased by name or other criteria. + */ + public function searchBy(Request $request): JsonResponse + { + try { + $search = $request->get('search', ''); + + if (empty($search)) { + return response()->json([ + 'message' => 'Le paramètre "search" est requis.', + ], 400); + } + + $deceased = $this->deceasedRepository->searchByName($search); + + return response()->json([ + 'data' => $deceased, + 'count' => $deceased->count(), + 'message' => $deceased->count() > 0 + ? 'Défunts trouvés avec succès.' + : 'Aucun défunt trouvé.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error searching deceased by name: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'search_term' => $search ?? '', + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recherche des défunts.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/DeceasedDocumentController.php b/thanasoft-back/app/Http/Controllers/Api/DeceasedDocumentController.php new file mode 100644 index 0000000..d281e10 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/DeceasedDocumentController.php @@ -0,0 +1,283 @@ +get('per_page', 15); + $filters = [ + 'search' => $request->get('search'), + 'deceased_id' => $request->get('deceased_id'), + 'doc_type' => $request->get('doc_type'), + 'file_id' => $request->get('file_id'), + 'sort_by' => $request->get('sort_by', 'created_at'), + 'sort_direction' => $request->get('sort_direction', 'desc'), + ]; + + // Remove null filters + $filters = array_filter($filters, function ($value) { + return $value !== null && $value !== ''; + }); + + $documents = $this->deceasedDocumentRepository->paginate($perPage, $filters); + + return new DeceasedDocumentCollection($documents); + + } catch (\Exception $e) { + Log::error('Erreur lors de la récupération des documents du défunt: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des documents du défunt.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get documents by deceased ID. + */ + public function byDeceased(string $deceasedId): DeceasedDocumentCollection|JsonResponse + { + try { + $documents = $this->deceasedDocumentRepository->getByDeceasedId((int) $deceasedId); + return new DeceasedDocumentCollection($documents); + } catch (\Exception $e) { + Log::error('Erreur lors de la récupération des documents par défunt: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'deceased_id' => $deceasedId, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des documents du défunt.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get documents by document type. + */ + public function byDocType(Request $request): DeceasedDocumentCollection|JsonResponse + { + try { + $docType = $request->get('doc_type'); + + if (!$docType) { + return response()->json([ + 'message' => 'Le paramètre doc_type est requis.', + ], 400); + } + + $documents = $this->deceasedDocumentRepository->getByDocType($docType); + return new DeceasedDocumentCollection($documents); + } catch (\Exception $e) { + Log::error('Erreur lors de la récupération des documents par type: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'doc_type' => $request->get('doc_type'), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des documents par type.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get documents by file ID. + */ + public function byFile(string $fileId): DeceasedDocumentCollection|JsonResponse + { + try { + $documents = $this->deceasedDocumentRepository->getByFileId((int) $fileId); + return new DeceasedDocumentCollection($documents); + } catch (\Exception $e) { + Log::error('Erreur lors de la récupération des documents par fichier: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'file_id' => $fileId, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des documents par fichier.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Search documents by various criteria. + */ + public function search(Request $request): DeceasedDocumentCollection|JsonResponse + { + try { + $criteria = [ + 'deceased_id' => $request->get('deceased_id'), + 'doc_type' => $request->get('doc_type'), + 'file_id' => $request->get('file_id'), + 'generated_from' => $request->get('generated_from'), + 'generated_to' => $request->get('generated_to'), + ]; + + // Remove null criteria + $criteria = array_filter($criteria, function ($value) { + return $value !== null && $value !== ''; + }); + + $documents = $this->deceasedDocumentRepository->search($criteria); + return new DeceasedDocumentCollection($documents); + } catch (\Exception $e) { + Log::error('Erreur lors de la recherche de documents: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'criteria' => $request->all(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recherche de documents.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created deceased document. + */ + public function store(StoreDeceasedDocumentRequest $request): DeceasedDocumentResource|JsonResponse + { + try { + $document = $this->deceasedDocumentRepository->create($request->validated()); + return new DeceasedDocumentResource($document); + } catch (\Exception $e) { + Log::error('Erreur lors de la création du document du défunt: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du document du défunt.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified deceased document. + */ + public function show(string $id): DeceasedDocumentResource|JsonResponse + { + try { + $document = $this->deceasedDocumentRepository->find($id); + + if (!$document) { + return response()->json([ + 'message' => 'Document du défunt non trouvé.', + ], 404); + } + + return new DeceasedDocumentResource($document); + } catch (\Exception $e) { + Log::error('Erreur lors de la récupération du document du défunt: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'document_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du document du défunt.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified deceased document. + */ + public function update(UpdateDeceasedDocumentRequest $request, string $id): DeceasedDocumentResource|JsonResponse + { + try { + $updated = $this->deceasedDocumentRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Document du défunt non trouvé ou échec de la mise à jour.', + ], 404); + } + + $document = $this->deceasedDocumentRepository->find($id); + return new DeceasedDocumentResource($document); + } catch (\Exception $e) { + Log::error('Erreur lors de la mise à jour du document du défunt: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'document_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du document du défunt.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified deceased document. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->deceasedDocumentRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Document du défunt non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Document du défunt supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Erreur lors de la suppression du document du défunt: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'document_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du document du défunt.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/EmployeeController.php b/thanasoft-back/app/Http/Controllers/Api/EmployeeController.php new file mode 100644 index 0000000..d2ea8a8 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/EmployeeController.php @@ -0,0 +1,423 @@ +get('per_page', 15); + + $filters = [ + 'search' => $request->get('search'), + 'active' => $request->get('active'), + 'sort_by' => $request->get('sort_by', 'last_name'), + 'sort_direction' => $request->get('sort_direction', 'asc'), + ]; + + // Remove null filters + $filters = array_filter($filters, function ($value) { + return $value !== null && $value !== ''; + }); + + $result = $this->employeeRepository->getPaginated($perPage, $filters); + + return response()->json([ + 'data' => new EmployeeCollection($result['employees']), + 'pagination' => $result['pagination'], + 'message' => 'Employés récupérés avec succès.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error fetching employees: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des employés.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display paginated employees. + */ + public function paginated(Request $request): JsonResponse + { + try { + $perPage = (int) $request->get('per_page', 15); + $result = $this->employeeRepository->getPaginated($perPage, []); + + return response()->json([ + 'data' => new EmployeeCollection($result['employees']), + 'pagination' => $result['pagination'], + 'message' => 'Employés récupérés avec succès.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error fetching paginated employees: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des employés.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get active employees only. + */ + public function active(): EmployeeCollection|JsonResponse + { + try { + $employees = $this->employeeRepository->getActive(); + return new EmployeeCollection($employees); + } catch (\Exception $e) { + Log::error('Error fetching active employees: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des employés actifs.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get employees with thanatopractitioner data. + */ + public function withThanatopractitioner(): EmployeeCollection|JsonResponse + { + try { + $employees = $this->employeeRepository->getWithThanatopractitioner(); + return new EmployeeCollection($employees); + } catch (\Exception $e) { + Log::error('Error fetching employees with thanatopractitioner: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des employés avec données thanatopractitioner.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get employee statistics. + */ + public function statistics(): JsonResponse + { + try { + $statistics = $this->employeeRepository->getStatistics(); + + return response()->json([ + 'data' => $statistics, + 'message' => 'Statistiques des employés récupérées avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error fetching employee statistics: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des statistiques.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created employee. + */ + public function store(StoreEmployeeRequest $request): EmployeeResource|JsonResponse + { + try { + $employee = $this->employeeRepository->create($request->validated()); + return new EmployeeResource($employee); + } catch (\Exception $e) { + Log::error('Error creating employee: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création de l\'employé.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified employee. + */ + public function show(string $id): EmployeeResource|JsonResponse + { + try { + $employee = $this->employeeRepository->find($id); + + if (!$employee) { + return response()->json([ + 'message' => 'Employé non trouvé.', + ], 404); + } + + return new EmployeeResource($employee); + } catch (\Exception $e) { + Log::error('Error fetching employee: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'employee_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération de l\'employé.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get the intervention agenda for a specific employee. + */ + public function agenda(Request $request, string $id): JsonResponse + { + $validator = Validator::make($request->all(), [ + 'month' => ['nullable', 'date_format:Y-m'], + 'start_date' => ['nullable', 'date'], + 'end_date' => ['nullable', 'date', 'after_or_equal:start_date'], + 'status' => ['nullable', 'string'], + 'type' => ['nullable', 'string'], + ]); + + if ($validator->fails()) { + return response()->json([ + 'message' => 'Paramètres d\'agenda invalides.', + 'errors' => $validator->errors(), + ], 422); + } + + try { + $employee = $this->employeeRepository->find($id); + + if (!$employee) { + return response()->json([ + 'message' => 'Employé non trouvé.', + ], 404); + } + + $employee->load('thanatopractitioner'); + + if (!$employee->thanatopractitioner) { + return response()->json([ + 'message' => 'Aucun agenda disponible pour cet employé.', + 'employee' => [ + 'id' => $employee->id, + 'full_name' => $employee->full_name, + 'job_title' => $employee->job_title, + ], + 'filters' => [ + 'month' => $request->input('month'), + 'start_date' => $request->input('start_date'), + 'end_date' => $request->input('end_date'), + 'status' => $request->input('status'), + 'type' => $request->input('type'), + ], + 'data' => [], + 'meta' => [ + 'total' => 0, + 'status_summary' => [], + ], + ], 200); + } + + [$startDate, $endDate] = $this->resolveAgendaPeriod( + $request->input('month'), + $request->input('start_date'), + $request->input('end_date') + ); + + $interventions = $employee->thanatopractitioner + ->interventions() + ->with([ + 'client', + 'deceased', + 'location', + 'quote', + 'practitioners.employee', + ]) + ->when($startDate, function ($query) use ($startDate) { + $query->where('scheduled_at', '>=', $startDate); + }) + ->when($endDate, function ($query) use ($endDate) { + $query->where('scheduled_at', '<=', $endDate); + }) + ->when($request->filled('status'), function ($query) use ($request) { + $query->where('status', $request->string('status')); + }) + ->when($request->filled('type'), function ($query) use ($request) { + $query->where('type', $request->string('type')); + }) + ->orderBy('scheduled_at') + ->get(); + + return response()->json([ + 'message' => 'Agenda employé récupéré avec succès.', + 'employee' => [ + 'id' => $employee->id, + 'full_name' => $employee->full_name, + 'job_title' => $employee->job_title, + 'thanatopractitioner_id' => $employee->thanatopractitioner->id, + ], + 'filters' => [ + 'month' => $request->input('month'), + 'start_date' => $startDate?->toDateTimeString(), + 'end_date' => $endDate?->toDateTimeString(), + 'status' => $request->input('status'), + 'type' => $request->input('type'), + ], + 'data' => InterventionResource::collection($interventions)->resolve(), + 'meta' => [ + 'total' => $interventions->count(), + 'status_summary' => $interventions + ->groupBy('status') + ->map(fn ($group) => $group->count()) + ->toArray(), + ], + ], 200); + } catch (\Exception $e) { + Log::error('Error fetching employee agenda: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'employee_id' => $id, + 'filters' => $request->all(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération de l\'agenda employé.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified employee. + */ + public function update(UpdateEmployeeRequest $request, string $id): EmployeeResource|JsonResponse + { + try { + $updated = $this->employeeRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Employé non trouvé ou échec de la mise à jour.', + ], 404); + } + + $employee = $this->employeeRepository->find($id); + return new EmployeeResource($employee); + } catch (\Exception $e) { + Log::error('Error updating employee: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'employee_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour de l\'employé.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified employee. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->employeeRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Employé non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Employé supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting employee: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'employee_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression de l\'employé.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Resolve the requested agenda period. + * + * Defaults to the current month when no explicit range is provided. + * + * @return array{0: \Carbon\Carbon, 1: \Carbon\Carbon} + */ + private function resolveAgendaPeriod(?string $month, ?string $startDate, ?string $endDate): array + { + if ($month) { + $reference = Carbon::createFromFormat('Y-m', $month)->startOfMonth(); + + return [$reference->copy()->startOfMonth(), $reference->copy()->endOfMonth()]; + } + + if ($startDate || $endDate) { + return [ + $startDate ? Carbon::parse($startDate)->startOfDay() : Carbon::now()->startOfMonth(), + $endDate ? Carbon::parse($endDate)->endOfDay() : Carbon::now()->endOfMonth(), + ]; + } + + return [Carbon::now()->startOfMonth(), Carbon::now()->endOfMonth()]; + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/FileAttachmentController.php b/thanasoft-back/app/Http/Controllers/Api/FileAttachmentController.php new file mode 100644 index 0000000..1fa56ce --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/FileAttachmentController.php @@ -0,0 +1,438 @@ +validate([ + 'file_id' => 'required|exists:files,id', + 'attachable_type' => 'required|string|in:App\Models\Intervention,App\Models\Client,App\Models\Deceased', + 'attachable_id' => 'required|integer', + 'label' => 'nullable|string|max:255', + 'sort_order' => 'nullable|integer|min:0', + ]); + + // Verify the attachable model exists + $attachableModel = $this->getAttachableModel($request->attachable_type, $request->attachable_id); + if (!$attachableModel) { + return response()->json([ + 'message' => 'Le modèle cible n\'existe pas.', + ], 404); + } + + // Check if file is already attached to this model + $existingAttachment = FileAttachment::where('file_id', $request->file_id) + ->where('attachable_type', $request->attachable_type) + ->where('attachable_id', $request->attachable_id) + ->first(); + + if ($existingAttachment) { + return response()->json([ + 'message' => 'Ce fichier est déjà attaché à cet élément.', + ], 422); + } + + DB::beginTransaction(); + + try { + // Create the attachment + $attachment = FileAttachment::create([ + 'file_id' => $request->file_id, + 'attachable_type' => $request->attachable_type, + 'attachable_id' => $request->attachable_id, + 'label' => $request->label, + 'sort_order' => $request->sort_order ?? 0, + ]); + + // Load relationships for response + $attachment->load('file'); + + DB::commit(); + + return response()->json([ + 'data' => new FileAttachmentResource($attachment), + 'message' => 'Fichier attaché avec succès.', + ], 201); + + } catch (\Exception $e) { + DB::rollBack(); + throw $e; + } + + } catch (\Exception $e) { + Log::error('Error attaching file: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'request_data' => $request->all(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de l\'attachement du fichier.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Detach a file from a model + */ + public function detach(Request $request, string $attachmentId): JsonResponse + { + try { + $attachment = FileAttachment::find($attachmentId); + + if (!$attachment) { + return response()->json([ + 'message' => 'Attachement de fichier non trouvé.', + ], 404); + } + + DB::beginTransaction(); + + try { + $attachment->delete(); + + DB::commit(); + + return response()->json([ + 'message' => 'Fichier détaché avec succès.', + ], 200); + + } catch (\Exception $e) { + DB::rollBack(); + throw $e; + } + + } catch (\Exception $e) { + Log::error('Error detaching file: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'attachment_id' => $attachmentId, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors du détachement du fichier.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update file attachment metadata + */ + public function update(Request $request, string $attachmentId): JsonResponse + { + try { + $request->validate([ + 'label' => 'nullable|string|max:255', + 'sort_order' => 'nullable|integer|min:0', + ]); + + $attachment = FileAttachment::find($attachmentId); + + if (!$attachment) { + return response()->json([ + 'message' => 'Attachement de fichier non trouvé.', + ], 404); + } + + DB::beginTransaction(); + + try { + $attachment->update($request->only(['label', 'sort_order'])); + $attachment->load('file'); + + DB::commit(); + + return response()->json([ + 'data' => new FileAttachmentResource($attachment), + 'message' => 'Attachement de fichier mis à jour avec succès.', + ], 200); + + } catch (\Exception $e) { + DB::rollBack(); + throw $e; + } + + } catch (\Exception $e) { + Log::error('Error updating file attachment: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'attachment_id' => $attachmentId, + 'request_data' => $request->all(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour de l\'attachement.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get files attached to a specific model + */ + public function getAttachedFiles(Request $request): JsonResponse + { + try { + $request->validate([ + 'attachable_type' => 'required|string|in:App\Models\Intervention,App\Models\Client,App\Models\Deceased', + 'attachable_id' => 'required|integer', + ]); + + $attachments = FileAttachment::where('attachable_type', $request->attachable_type) + ->where('attachable_id', $request->attachable_id) + ->with('file') + ->orderBy('sort_order') + ->orderBy('created_at') + ->get(); + + return response()->json([ + 'data' => FileAttachmentResource::collection($attachments), + 'count' => $attachments->count(), + 'message' => 'Fichiers attachés récupérés avec succès.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error getting attached files: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'request_data' => $request->all(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des fichiers attachés.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get files attached to an intervention + */ + public function getInterventionFiles(Request $request, int $interventionId): JsonResponse + { + try { + $intervention = Intervention::find($interventionId); + + if (!$intervention) { + return response()->json([ + 'message' => 'Intervention non trouvée.', + ], 404); + } + + $attachments = $intervention->fileAttachments()->with('file')->get(); + + return response()->json([ + 'data' => FileAttachmentResource::collection($attachments), + 'count' => $attachments->count(), + 'message' => 'Fichiers de l\'intervention récupérés avec succès.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error getting intervention files: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'intervention_id' => $interventionId, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des fichiers de l\'intervention.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get files attached to a client + */ + public function getClientFiles(Request $request, int $clientId): JsonResponse + { + try { + $client = Client::find($clientId); + + if (!$client) { + return response()->json([ + 'message' => 'Client non trouvé.', + ], 404); + } + + $attachments = $client->fileAttachments()->with('file')->get(); + + return response()->json([ + 'data' => FileAttachmentResource::collection($attachments), + 'count' => $attachments->count(), + 'message' => 'Fichiers du client récupérés avec succès.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error getting client files: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_id' => $clientId, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des fichiers du client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get files attached to a deceased + */ + public function getDeceasedFiles(Request $request, int $deceasedId): JsonResponse + { + try { + $deceased = Deceased::find($deceasedId); + + if (!$deceased) { + return response()->json([ + 'message' => 'Défunt non trouvé.', + ], 404); + } + + $attachments = $deceased->fileAttachments()->with('file')->get(); + + return response()->json([ + 'data' => FileAttachmentResource::collection($attachments), + 'count' => $attachments->count(), + 'message' => 'Fichiers du défunt récupérés avec succès.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error getting deceased files: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'deceased_id' => $deceasedId, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des fichiers du défunt.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Detach multiple files at once + */ + public function detachMultiple(Request $request): JsonResponse + { + try { + $request->validate([ + 'attachment_ids' => 'required|array|min:1', + 'attachment_ids.*' => 'exists:file_attachments,id', + ]); + + DB::beginTransaction(); + + try { + $deletedCount = FileAttachment::whereIn('id', $request->attachment_ids)->delete(); + + DB::commit(); + + return response()->json([ + 'deleted_count' => $deletedCount, + 'message' => $deletedCount . ' fichier(s) détaché(s) avec succès.', + ], 200); + + } catch (\Exception $e) { + DB::rollBack(); + throw $e; + } + + } catch (\Exception $e) { + Log::error('Error detaching multiple files: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'attachment_ids' => $request->attachment_ids ?? [], + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors du détachement des fichiers.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Reorder file attachments + */ + public function reorder(Request $request): JsonResponse + { + try { + $request->validate([ + 'attachments' => 'required|array|min:1', + 'attachments.*.id' => 'required|exists:file_attachments,id', + 'attachments.*.sort_order' => 'required|integer|min:0', + ]); + + DB::beginTransaction(); + + try { + foreach ($request->attachments as $attachmentData) { + FileAttachment::where('id', $attachmentData['id']) + ->update(['sort_order' => $attachmentData['sort_order']]); + } + + DB::commit(); + + return response()->json([ + 'message' => 'Ordre des fichiers mis à jour avec succès.', + ], 200); + + } catch (\Exception $e) { + DB::rollBack(); + throw $e; + } + + } catch (\Exception $e) { + Log::error('Error reordering file attachments: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'attachments' => $request->attachments ?? [], + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la réorganisation des fichiers.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get the attachable model instance + */ + private function getAttachableModel(string $type, int $id): ?Model + { + return match ($type) { + Intervention::class => Intervention::find($id), + Client::class => Client::find($id), + Deceased::class => Deceased::find($id), + default => null, + }; + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/FileController.php b/thanasoft-back/app/Http/Controllers/Api/FileController.php new file mode 100644 index 0000000..b8f3859 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/FileController.php @@ -0,0 +1,444 @@ +get('per_page', 15); + $filters = [ + 'search' => $request->get('search'), + 'mime_type' => $request->get('mime_type'), + 'uploaded_by' => $request->get('uploaded_by'), + 'category' => $request->get('category'), + 'client_id' => $request->get('client_id'), + 'date_from' => $request->get('date_from'), + 'date_to' => $request->get('date_to'), + 'sort_by' => $request->get('sort_by', 'uploaded_at'), + 'sort_direction' => $request->get('sort_direction', 'desc'), + ]; + + // Remove null filters + $filters = array_filter($filters, function ($value) { + return $value !== null && $value !== ''; + }); + + $files = $this->fileRepository->paginate($perPage, $filters); + + return new FileCollection($files); + + } catch (\Exception $e) { + Log::error('Error fetching files: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des fichiers.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly uploaded file. + */ + public function store(StoreFileRequest $request): FileResource|JsonResponse + { + try { + $validatedData = $request->validated(); + $file = $request->file('file'); + + // Generate organized storage path + $storagePath = $this->generateOrganizedPath( + $validatedData['category'], + $validatedData['client_id'] ?? null, + $validatedData['subcategory'] ?? null, + $validatedData['file_name'] + ); + + // Store the file + $storedFilePath = $file->store($storagePath, 'public'); + + // Calculate SHA256 hash + $hash = hash_file('sha256', $file->path()); + + // Prepare data for database + $fileData = array_merge($validatedData, [ + 'storage_uri' => $storedFilePath, + 'sha256' => $hash, + 'uploaded_by' => $request->user()->id, + 'uploaded_at' => now(), + ]); + + $createdFile = $this->fileRepository->create($fileData); + + return new FileResource($createdFile); + + } catch (\Exception $e) { + Log::error('Error uploading file: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'user_id' => $request->user()->id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de l\'upload du fichier.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified file. + */ + public function show(string $id): FileResource|JsonResponse + { + try { + $file = $this->fileRepository->find($id); + + if (!$file) { + return response()->json([ + 'message' => 'Fichier non trouvé.', + ], 404); + } + + return new FileResource($file); + } catch (\Exception $e) { + Log::error('Error fetching file: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'file_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du fichier.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified file metadata. + */ + public function update(UpdateFileRequest $request, string $id): FileResource|JsonResponse + { + try { + $file = $this->fileRepository->find($id); + + if (!$file) { + return response()->json([ + 'message' => 'Fichier non trouvé.', + ], 404); + } + + $validatedData = $request->validated(); + + // If category or client changed, move the file + if (isset($validatedData['category']) || isset($validatedData['client_id']) || isset($validatedData['subcategory'])) { + $newStoragePath = $this->generateOrganizedPath( + $validatedData['category'] ?? $this->extractCategoryFromPath($file->storage_uri), + $validatedData['client_id'] ?? $this->extractClientFromPath($file->storage_uri), + $validatedData['subcategory'] ?? $this->extractSubcategoryFromPath($file->storage_uri), + $file->file_name + ); + + if ($newStoragePath !== $file->storage_uri) { + // Move file to new location + Storage::disk('public')->move($file->storage_uri, $newStoragePath); + $validatedData['storage_uri'] = $newStoragePath; + } + } + + $updated = $this->fileRepository->update($id, $validatedData); + + if (!$updated) { + return response()->json([ + 'message' => 'Fichier non trouvé ou échec de la mise à jour.', + ], 404); + } + + $updatedFile = $this->fileRepository->find($id); + return new FileResource($updatedFile); + } catch (\Exception $e) { + Log::error('Error updating file: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'file_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du fichier.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified file. + */ + public function destroy(string $id): JsonResponse + { + try { + $file = $this->fileRepository->find($id); + + if (!$file) { + return response()->json([ + 'message' => 'Fichier non trouvé.', + ], 404); + } + + // Delete file from storage + Storage::disk('public')->delete($file->storage_uri); + + // Delete from database + $deleted = $this->fileRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Fichier non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Fichier supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting file: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'file_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du fichier.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get files by category. + */ + public function byCategory(Request $request, string $category): FileCollection|JsonResponse + { + try { + $perPage = $request->get('per_page', 15); + $files = $this->fileRepository->getByCategory($category, $perPage); + + return new FileCollection($files); + } catch (\Exception $e) { + Log::error('Error fetching files by category: ' . $e->getMessage(), [ + 'exception' => $e, + 'category' => $category, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des fichiers par catégorie.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get files by client. + */ + public function byClient(Request $request, int $clientId): FileCollection|JsonResponse + { + try { + $perPage = $request->get('per_page', 15); + $files = $this->fileRepository->getByClient($clientId, $perPage); + + return new FileCollection($files); + } catch (\Exception $e) { + Log::error('Error fetching files by client: ' . $e->getMessage(), [ + 'exception' => $e, + 'client_id' => $clientId, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des fichiers du client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get organized file structure. + */ + public function organized(): JsonResponse + { + try { + $organizedFiles = $this->fileRepository->getOrganizedFiles(); + + return response()->json([ + 'data' => $organizedFiles, + 'message' => 'Structure de fichiers récupérée avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error fetching organized files: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération de la structure de fichiers.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get storage statistics. + */ + public function stats(): JsonResponse + { + try { + $stats = $this->fileRepository->getStorageStats(); + + return response()->json([ + 'data' => $stats, + 'message' => 'Statistiques de stockage récupérées avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error fetching storage stats: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des statistiques.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Download a file. + */ + public function download(string $id): JsonResponse + { + try { + $file = $this->fileRepository->find($id); + + if (!$file) { + return response()->json([ + 'message' => 'Fichier non trouvé.', + ], 404); + } + + if (!Storage::disk('public')->exists($file->storage_uri)) { + return response()->json([ + 'message' => 'Fichier physique non trouvé sur le stockage.', + ], 404); + } + + $downloadUrl = Storage::disk('public')->url($file->storage_uri); + + return response()->json([ + 'data' => [ + 'download_url' => $downloadUrl, + 'file_name' => $file->file_name, + 'mime_type' => $file->mime_type, + ], + 'message' => 'URL de téléchargement générée avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error generating download URL: ' . $e->getMessage(), [ + 'exception' => $e, + 'file_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la génération de l\'URL de téléchargement.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Generate organized storage path. + */ + private function generateOrganizedPath(string $category, ?int $clientId, ?string $subcategory, string $fileName): string + { + $pathParts = []; + + if ($clientId) { + $pathParts[] = 'client'; + $pathParts[] = $clientId; + } else { + $pathParts[] = 'general'; + } + + $pathParts[] = $category; + + if ($subcategory) { + $pathParts[] = Str::slug($subcategory); + } else { + $pathParts[] = 'files'; + } + + // Add timestamp to avoid conflicts + $timestamp = now()->format('Y-m-d_H-i-s'); + $extension = pathinfo($fileName, PATHINFO_EXTENSION); + $basename = pathinfo($fileName, PATHINFO_FILENAME); + $safeFilename = Str::slug($basename) . '_' . $timestamp . '.' . $extension; + + $pathParts[] = $safeFilename; + + return implode('/', $pathParts); + } + + /** + * Extract category from storage path. + */ + private function extractCategoryFromPath(string $storageUri): string + { + $pathParts = explode('/', $storageUri); + return $pathParts[count($pathParts) - 3] ?? 'general'; + } + + /** + * Extract client ID from storage path. + */ + private function extractClientFromPath(string $storageUri): ?int + { + $pathParts = explode('/', $storageUri); + if (count($pathParts) >= 4 && $pathParts[0] === 'client') { + return (int) $pathParts[1]; + } + return null; + } + + /** + * Extract subcategory from storage path. + */ + private function extractSubcategoryFromPath(string $storageUri): ?string + { + $pathParts = explode('/', $storageUri); + return $pathParts[count($pathParts) - 2] ?? null; + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/FournisseurController.php b/thanasoft-back/app/Http/Controllers/Api/FournisseurController.php new file mode 100644 index 0000000..e916eb7 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/FournisseurController.php @@ -0,0 +1,208 @@ +get('per_page', 15); + $filters = [ + 'search' => $request->get('search'), + 'is_active' => $request->get('is_active'), + 'sort_by' => $request->get('sort_by', 'created_at'), + 'sort_direction' => $request->get('sort_direction', 'desc'), + ]; + + // Remove null filters + $filters = array_filter($filters, function ($value) { + return $value !== null && $value !== ''; + }); + + $fournisseurs = $this->fournisseurRepository->paginate($perPage, $filters); + + return new FournisseurCollection($fournisseurs); + + } catch (\Exception $e) { + Log::error('Error fetching fournisseurs: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des fournisseurs.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created fournisseur. + */ + public function store(StoreFournisseurRequest $request): FournisseurResource|JsonResponse + { + try { + $fournisseur = $this->fournisseurRepository->create($request->validated()); + return new FournisseurResource($fournisseur); + } catch (\Exception $e) { + Log::error('Error creating fournisseur: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du fournisseur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified fournisseur. + */ + public function show(string $id): FournisseurResource|JsonResponse + { + try { + $fournisseur = $this->fournisseurRepository->find($id); + + if (!$fournisseur) { + return response()->json([ + 'message' => 'Fournisseur non trouvé.', + ], 404); + } + + return new FournisseurResource($fournisseur); + } catch (\Exception $e) { + Log::error('Error fetching fournisseur: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'fournisseur_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du fournisseur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function searchBy(Request $request): JsonResponse + { + try { + $name = $request->get('name', ''); + + if (empty($name)) { + return response()->json([ + 'message' => 'Le paramètre "name" est requis.', + ], 400); + } + + $fournisseurs = $this->fournisseurRepository->searchByName($name); + + return response()->json([ + 'data' => $fournisseurs, + 'count' => $fournisseurs->count(), + 'message' => $fournisseurs->count() > 0 + ? 'Fournisseurs trouvés avec succès.' + : 'Aucun fournisseur trouvé.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error searching fournisseurs by name: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'search_term' => $name, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recherche des fournisseurs.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified fournisseur. + */ + public function update(UpdateFournisseurRequest $request, string $id): FournisseurResource|JsonResponse + { + try { + $updated = $this->fournisseurRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Fournisseur non trouvé ou échec de la mise à jour.', + ], 404); + } + + $fournisseur = $this->fournisseurRepository->find($id); + return new FournisseurResource($fournisseur); + } catch (\Exception $e) { + Log::error('Error updating fournisseur: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'fournisseur_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du fournisseur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified fournisseur. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->fournisseurRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Fournisseur non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Fournisseur supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting fournisseur: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'fournisseur_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du fournisseur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/GoodsReceiptController.php b/thanasoft-back/app/Http/Controllers/Api/GoodsReceiptController.php new file mode 100644 index 0000000..db6ffa2 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/GoodsReceiptController.php @@ -0,0 +1,159 @@ +goodsReceiptRepository->all(); + return response()->json([ + 'data' => GoodsReceiptResource::collection($goodsReceipts), + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error fetching goods receipts: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des réceptions de marchandises.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created goods receipt. + */ + public function store(StoreGoodsReceiptRequest $request): JsonResponse + { + try { + $payload = $request->validated(); + + if (empty($payload['lines']) && !empty($payload['purchase_order_id'])) { + $purchaseOrder = PurchaseOrder::query() + ->with('lines') + ->find($payload['purchase_order_id']); + + if ($purchaseOrder) { + $payload['lines'] = $purchaseOrder->lines + ->filter(fn($line) => !empty($line->product_id)) + ->map(fn($line) => [ + 'product_id' => (int) $line->product_id, + 'packaging_id' => null, + 'packages_qty_received' => null, + 'units_qty_received' => (float) $line->quantity, + 'qty_received_base' => (float) $line->quantity, + 'unit_price' => (float) $line->unit_price, + 'unit_price_per_package' => null, + 'tva_rate_id' => null, + ]) + ->values() + ->all(); + } + } + + $goodsReceipt = $this->goodsReceiptRepository->create($payload); + return response()->json([ + 'data' => new GoodsReceiptResource($goodsReceipt), + 'message' => 'Réception de marchandise créée avec succès.', + 'status' => 'success' + ], 201); + } catch (\Exception $e) { + Log::error('Error creating goods receipt: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création de la réception de marchandise.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified goods receipt. + */ + public function show(string $id): JsonResponse + { + try { + $goodsReceipt = $this->goodsReceiptRepository->find((int) $id); + if (!$goodsReceipt) { + return response()->json(['message' => 'Réception de marchandise non trouvée.'], 404); + } + return response()->json([ + 'data' => new GoodsReceiptResource($goodsReceipt), + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error fetching goods receipt: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération de la réception de marchandise.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified goods receipt. + */ + public function update(UpdateGoodsReceiptRequest $request, string $id): JsonResponse + { + try { + $updated = $this->goodsReceiptRepository->update((int) $id, $request->validated()); + if (!$updated) { + return response()->json(['message' => 'Réception de marchandise non trouvée ou échec de la mise à jour.'], 404); + } + $goodsReceipt = $this->goodsReceiptRepository->find((int) $id); + return response()->json([ + 'data' => new GoodsReceiptResource($goodsReceipt), + 'message' => 'Réception de marchandise mise à jour avec succès.', + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error updating goods receipt: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour de la réception de marchandise.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified goods receipt. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->goodsReceiptRepository->delete((int) $id); + if (!$deleted) { + return response()->json(['message' => 'Réception de marchandise non trouvée ou échec de la suppression.'], 404); + } + return response()->json([ + 'message' => 'Réception de marchandise supprimée avec succès.', + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error deleting goods receipt: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression de la réception de marchandise.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/InterventionController.php b/thanasoft-back/app/Http/Controllers/Api/InterventionController.php new file mode 100644 index 0000000..57592a6 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/InterventionController.php @@ -0,0 +1,713 @@ +interventionRepository = $interventionRepository; + $this->interventionPractitionerRepository = $interventionPractitionerRepository; + $this->clientRepository = $clientRepository; + $this->contactRepository = $contactRepository; + $this->deceasedRepository = $deceasedRepository; + $this->quoteRepository = $quoteRepository; + $this->productRepository = $productRepository; + } + + /** + * Display a listing of the resource. + */ + public function index(Request $request): JsonResponse + { + try { + $filters = $request->only([ + 'client_id', + 'deceased_id', + 'status', + 'type', + 'start_date', + 'end_date', + 'sort_by', + 'sort_order' + ]); + + $perPage = $request->input('per_page', 15); + + $interventions = $this->interventionRepository->getAllPaginated($filters, $perPage); + + return response()->json(new InterventionCollection($interventions)); + } + catch (\Exception $e) { + Log::error('Error fetching interventions list: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des interventions.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Store a newly created resource in storage. + */ + public function store(StoreInterventionRequest $request): JsonResponse + { + try { + $validated = $request->validated(); + + $intervention = $this->interventionRepository->create($validated); + + return response()->json(new InterventionResource($intervention), Response::HTTP_CREATED); + } + catch (\Exception $e) { + Log::error('Error creating intervention: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création de l\'intervention.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Create an intervention with all related data (deceased, client, contact, location, documents). + */ + public function createInterventionalldata(StoreInterventionWithAllDataRequest $request): JsonResponse + { + try { + $validated = $request->validated(); + + // Wrap everything in a database transaction + $result = DB::transaction(function () use ($validated) { + // Step 1: Handle Deceased (Create or Link) + $deceased = null; + if (!empty($validated['deceased_id'])) { + $deceased = $this->deceasedRepository->findById($validated['deceased_id']); + } + else { + $deceasedData = $validated['deceased']; + $deceased = $this->deceasedRepository->create($deceasedData); + } + + // Step 2: Link existing client or create a new one + if (!empty($validated['client_id'])) { + $client = $this->clientRepository->find($validated['client_id']); + } + else { + $clientData = $validated['client']; + $client = $this->clientRepository->create($clientData); + } + + // Step 3: Create the contact (if provided) + $contactId = null; + if (!empty($validated['contact'])) { + $contactData = array_merge($validated['contact'], [ + 'client_id' => $client->id + ]); + $contact = $this->contactRepository->create($contactData); + $contactId = $contact->id; + } + + // Step 4: Handle Location + $locationData = $validated['location'] ?? []; + $locationId = $validated['location_id'] ?? null; + $locationNotes = ''; + + if (!$locationId && !empty($locationData)) { + // Create new location for the client + $locData = array_merge($locationData, [ + 'client_id' => $client->id, + 'is_default' => false + ]); + $newLocation = $this->clientLocationRepository->create($locData); + $locationId = $newLocation->id; + } + + if ($locationId) { + // Fetch location to add details to notes if needed, or just rely on relation. + // For now, let's keep the legacy behavior of adding text to notes for quick reference, + // but also link the ID. Use the provided data or fetch? + // If we have an ID, we might not have the text data in $locationData if it came from search. + // So we only append text notes if we have $locationData (Create mode or if frontend sends it). + } + + if (!empty($locationData)) { + $locationParts = []; + if (!empty($locationData['name'])) { + $locationParts[] = 'Lieu: ' . $locationData['name']; + } + if (!empty($locationData['address'])) { + $locationParts[] = 'Adresse: ' . $locationData['address']; + } + if (!empty($locationData['city'])) { + $locationParts[] = 'Ville: ' . $locationData['city']; + } + if (!empty($locationData['access_instructions'])) { + $locationParts[] = 'Instructions: ' . $locationData['access_instructions']; + } + if (!empty($locationData['notes'])) { + $locationParts[] = 'Notes: ' . $locationData['notes']; + } + $locationNotes = !empty($locationParts) ? "\n\n" . implode("\n", $locationParts) : ''; + } + + // Step 5: Create the intervention + $interventionData = array_merge($validated['intervention'], [ + 'deceased_id' => $deceased->id, + 'client_id' => $client->id, + 'location_id' => $locationId, + 'notes' => ($validated['intervention']['notes'] ?? '') . $locationNotes + ]); + $intervention = $this->interventionRepository->create($interventionData); + + // Step 5a: Assign practitioners if provided + if (!empty($validated['intervention']['principal_practitioner_id'])) { + $this->interventionPractitionerRepository->createAssignment( + $intervention->id, + (int)$validated['intervention']['principal_practitioner_id'], + 'principal' + ); + } + + if (!empty($validated['intervention']['assistant_practitioner_ids']) && is_array($validated['intervention']['assistant_practitioner_ids'])) { + foreach ($validated['intervention']['assistant_practitioner_ids'] as $assistantId) { + $this->interventionPractitionerRepository->createAssignment( + $intervention->id, + (int)$assistantId, + 'assistant' + ); + } + } + + if ( + empty($validated['intervention']['principal_practitioner_id']) && + !empty($validated['intervention']['practitioners']) && + is_array($validated['intervention']['practitioners']) + ) { + foreach ($validated['intervention']['practitioners'] as $index => $practitionerId) { + $role = $index === 0 ? 'principal' : 'assistant'; + $this->interventionPractitionerRepository->createAssignment( + $intervention->id, + (int)$practitionerId, + $role + ); + } + } + + $intervention->load('practitioners'); + + // Step 5b: Create a Quote for this intervention + try { + $interventionProduct = $this->productRepository->findInterventionProduct(); + + if ($interventionProduct) { + // Calculate totals + $quantity = 1; + // Ideally fetch TVA rate from product, default to 20% if not set + // Assuming product has tva_rate relationship or simple logic + $tvaRateValue = 20; + + $unitPrice = $interventionProduct->prix_unitaire; + $totalHt = $unitPrice * $quantity; + $totalTva = $totalHt * ($tvaRateValue / 100); + $totalTtc = $totalHt + $totalTva; + + $quoteData = [ + 'client_id' => $client->id, + 'status' => 'brouillon', + 'quote_date' => now()->toDateString(), + 'currency' => 'EUR', + 'valid_until' => now()->addDays(30)->toDateString(), + 'total_ht' => $totalHt, + 'total_tva' => $totalTva, + 'total_ttc' => $totalTtc, + 'lines' => [ + [ + 'product_id' => $interventionProduct->id, + 'description' => 'Intervention: ' . ($intervention->type ?? 'Standard'), + 'units_qty' => $quantity, + 'unit_price' => $unitPrice, + 'discount_pct' => 0, + 'total_ht' => $totalHt, + // 'tva_rate_id' => ... if needed + ] + ] + ]; + + $quote = $this->quoteRepository->create($quoteData); + + // Update the intervention with the newly created quote ID + $intervention->update(['quote_id' => $quote->id]); + + Log::info('Quote auto-created for intervention', ['intervention_id' => $intervention->id, 'quote_id' => $quote->id]); + } + else { + Log::warning('No intervention product found, skipping auto-quote creation', ['intervention_id' => $intervention->id]); + } + + } + catch (\Exception $e) { + Log::error('Failed to auto-create quote for intervention: ' . $e->getMessage()); + // Silently fail for the quote part to not block intervention creation + } + + + // Step 6: Handle document uploads (if any) + $documents = $validated['documents'] ?? []; + if (!empty($documents)) { + foreach ($documents as $documentData) { + if (isset($documentData['file']) && $documentData['file']->isValid()) { + // Store the file and create intervention attachment + // This is a placeholder - implement actual file upload logic + // $path = $documentData['file']->store('intervention_documents'); + // Create intervention attachment record + } + } + } + + // Return all created data + return [ + 'intervention' => $intervention, + 'deceased' => $deceased, + 'client' => $client, + 'contact_id' => $contactId, + 'documents_count' => count($documents) + ]; + }); + + Log::info('Intervention with all data created successfully', [ + 'intervention_id' => $result['intervention']->id, + 'deceased_id' => $result['deceased']->id, + 'client_id' => $result['client']->id, + 'documents_count' => $result['documents_count'] + ]); + + return response()->json([ + 'message' => 'Intervention créée avec succès', + 'data' => [ + 'intervention' => new InterventionResource($result['intervention']), + 'deceased' => $result['deceased'], + 'client' => $result['client'], + 'contact_id' => $result['contact_id'], + 'documents_count' => $result['documents_count'] + ] + ], Response::HTTP_CREATED); + + } + catch (\Illuminate\Validation\ValidationException $e) { + // Validation errors are handled by the FormRequest + return response()->json([ + 'message' => 'Données invalides', + 'errors' => $e->errors() + ], Response::HTTP_UNPROCESSABLE_ENTITY); + } + catch (\Exception $e) { + Log::error('Error creating intervention with all data: ' . $e->getMessage(), [ + 'trace' => $e->getTraceAsString(), + 'input' => $request->except(['documents']) // Don't log file data + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création de l\'intervention.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Display the specified resource. + */ + public function show(int $id): JsonResponse + { + try { + $intervention = $this->interventionRepository->findById($id); + + return response()->json(new InterventionResource($intervention)); + } + catch (\Exception $e) { + Log::error('Error fetching intervention details: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Intervention non trouvée ou une erreur est survenue.', + 'error' => $e->getMessage() + ], Response::HTTP_NOT_FOUND); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(UpdateInterventionRequest $request, int $id): JsonResponse + { + try { + $intervention = $this->interventionRepository->findById($id); + + $validated = $request->validated(); + + $updatedIntervention = $this->interventionRepository->update($intervention, $validated); + + return response()->json(new InterventionResource($updatedIntervention)); + } + catch (\Exception $e) { + Log::error('Error updating intervention: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour de l\'intervention.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(int $id): JsonResponse + { + try { + $intervention = $this->interventionRepository->findById($id); + + $this->interventionRepository->delete($intervention); + + return response()->json(null, Response::HTTP_NO_CONTENT); + } + catch (\Exception $e) { + Log::error('Error deleting intervention: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression de l\'intervention.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Change the status of an intervention. + */ + public function changeStatus(Request $request, int $id): JsonResponse + { + try { + $validated = $request->validate([ + 'status' => 'required|in:demande,planifie,en_cours,termine,annule' + ]); + + $intervention = $this->interventionRepository->findById($id); + + $updatedIntervention = $this->interventionRepository->changeStatus( + $intervention, + $validated['status'] + ); + + return response()->json(new InterventionResource($updatedIntervention)); + } + catch (\Exception $e) { + Log::error('Error changing intervention status: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la modification du statut de l\'intervention.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Get interventions for a specific month. + */ + public function byMonth(Request $request): JsonResponse + { + try { + $validated = $request->validate([ + 'year' => 'required|integer|min:2000|max:2100', + 'month' => 'required|integer|min:1|max:12' + ]); + + $interventions = $this->interventionRepository->getByMonth( + $validated['year'], + $validated['month'] + ); + + return response()->json([ + 'data' => $interventions->map(function ($intervention) { + return new InterventionResource($intervention); + }), + 'meta' => [ + 'total' => $interventions->count(), + 'year' => $validated['year'], + 'month' => $validated['month'], + ] + ]); + } + catch (\Exception $e) { + Log::error('Error fetching interventions by month: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des interventions du mois.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Create assignment of practitioners to an intervention + * + * @param Request $request + * @param int $id + * @return JsonResponse + */ + public function createAssignment(Request $request, int $id): JsonResponse + { + try { + $validated = $request->validate([ + 'principal_practitioner_id' => 'nullable|integer|exists:thanatopractitioners,id', + 'assistant_practitioner_ids' => 'nullable|array', + 'assistant_practitioner_ids.*' => 'integer|exists:thanatopractitioners,id', + ]); + + + $intervention = $this->interventionRepository->findById($id); + + if (!$intervention) { + return response()->json([ + 'message' => 'Intervention non trouvée.' + ], Response::HTTP_NOT_FOUND); + } + + // Remove existing principal practitioner first + if (isset($validated['principal_practitioner_id'])) { + $principalId = $validated['principal_practitioner_id']; + $this->interventionPractitionerRepository->createAssignment($id, $principalId, 'principal'); + } + + // Handle assistant practitioners + if (isset($validated['assistant_practitioner_ids']) && is_array($validated['assistant_practitioner_ids'])) { + foreach ($validated['assistant_practitioner_ids'] as $assistantId) { + $this->interventionPractitionerRepository->createAssignment($id, $assistantId, 'assistant'); + } + } + + // Load the intervention with practitioners to return updated data + $intervention->load('practitioners'); + $practitioners = $intervention->practitioners; + + return response()->json([ + 'data' => new InterventionResource($intervention), + 'message' => 'Assignment(s) créé(s) avec succès.', + 'practitioners_count' => $practitioners->count(), + 'practitioners' => $practitioners->map(function ($p) { + return [ + 'id' => $p->id, + 'full_name' => $p->full_name ?? ($p->first_name . ' ' . $p->last_name), + 'role' => $p->pivot->role ?? 'unknown' + ]; + })->toArray() + ], Response::HTTP_OK); + + } + catch (\Exception $e) { + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création de l\'assignment.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Unassign a practitioner from an intervention + * + * @param Request $request + * @param int $interventionId + * @param int $practitionerId + * @return JsonResponse + */ + public function unassignPractitioner(Request $request, int $interventionId, int $practitionerId): JsonResponse + { + try { + Log::info('Unassigning practitioner from intervention', [ + 'intervention_id' => $interventionId, + 'practitioner_id' => $practitionerId + ]); + + // Validate that the intervention exists + $intervention = $this->interventionRepository->findById($interventionId); + + if (!$intervention) { + return response()->json([ + 'message' => 'Intervention non trouvée.' + ], Response::HTTP_NOT_FOUND); + } + + // Check if the practitioner is actually assigned to this intervention + $isAssigned = $this->interventionPractitionerRepository->isPractitionerAssigned($interventionId, $practitionerId); + + if (!$isAssigned) { + return response()->json([ + 'message' => 'Le praticien n\'est pas assigné à cette intervention.' + ], Response::HTTP_NOT_FOUND); + } + + // Remove the practitioner assignment + $deleted = $this->interventionPractitionerRepository->removeAssignment($interventionId, $practitionerId); + + if ($deleted > 0) { + Log::info('Practitioner unassigned successfully', [ + 'intervention_id' => $interventionId, + 'practitioner_id' => $practitionerId, + 'deleted_records' => $deleted + ]); + + // Reload intervention with remaining practitioners + $intervention->load('practitioners'); + $remainingPractitioners = $intervention->practitioners; + + return response()->json([ + 'data' => new InterventionResource($intervention), + 'message' => 'Praticien désassigné avec succès.', + 'remaining_practitioners_count' => $remainingPractitioners->count(), + 'remaining_practitioners' => $remainingPractitioners->map(function ($p) { + return [ + 'id' => $p->id, + 'employee_name' => $p->employee->full_name ?? ($p->employee->first_name . ' ' . $p->employee->last_name), + 'role' => $p->pivot->role ?? 'unknown' + ]; + })->toArray() + ], Response::HTTP_OK); + } + else { + Log::warning('No practitioner assignment found to delete', [ + 'intervention_id' => $interventionId, + 'practitioner_id' => $practitionerId + ]); + + return response()->json([ + 'message' => 'Aucun assignment de praticien trouvé à supprimer.' + ], Response::HTTP_NOT_FOUND); + } + + } + catch (\Exception $e) { + Log::error('Error unassigning practitioner from intervention: ' . $e->getMessage(), [ + 'intervention_id' => $interventionId, + 'practitioner_id' => $practitionerId, + 'request_data' => $request->all(), + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la désassignation du praticien.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Debug endpoint to check practitioners in database + */ + public function debugPractitioners(int $id): JsonResponse + { + try { + $intervention = $this->interventionRepository->findById($id); + + // Direct database query + $dbPractitioners = DB::table('intervention_practitioner') + ->where('intervention_id', $id) + ->get(); + + // Eager loaded practitioners + $eagerPractitioners = $intervention->practitioners()->get(); + + return response()->json([ + 'intervention_id' => $id, + 'database_records' => $dbPractitioners, + 'eager_loaded_count' => $eagerPractitioners->count(), + 'eager_loaded_data' => $eagerPractitioners->map(function ($p) { + return [ + 'id' => $p->id, + 'full_name' => $p->full_name ?? ($p->first_name . ' ' . $p->last_name), + 'role' => $p->pivot->role ?? 'unknown' + ]; + })->toArray() + ]); + + } + catch (\Exception $e) { + Log::error('Error in debug practitioners: ' . $e->getMessage()); + return response()->json(['error' => $e->getMessage()], 500); + } + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Http/Controllers/Api/InvoiceController.php b/thanasoft-back/app/Http/Controllers/Api/InvoiceController.php new file mode 100644 index 0000000..1b45420 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/InvoiceController.php @@ -0,0 +1,222 @@ +invoiceRepository->all(); + return InvoiceResource::collection($invoices); + } catch (\Exception $e) { + Log::error('Error fetching invoices: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des factures.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created invoice. + */ + public function store(StoreInvoiceRequest $request): InvoiceResource|JsonResponse + { + try { + $invoice = $this->invoiceRepository->create($request->validated()); + return new InvoiceResource($invoice); + } catch (\Exception $e) { + Log::error('Error creating invoice: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création de la facture.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified invoice. + */ + public function show(string $id): InvoiceResource|JsonResponse + { + try { + $invoice = $this->invoiceRepository->find($id); + + if (! $invoice) { + return response()->json([ + 'message' => 'Facture non trouvée.', + ], 404); + } + + return new InvoiceResource($invoice); + } catch (\Exception $e) { + Log::error('Error fetching invoice: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'invoice_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération de la facture.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified invoice. + */ + public function update(UpdateInvoiceRequest $request, string $id): InvoiceResource|JsonResponse + { + try { + $updated = $this->invoiceRepository->update($id, $request->validated()); + + if (! $updated) { + return response()->json([ + 'message' => 'Facture non trouvée ou échec de la mise à jour.', + ], 404); + } + + $invoice = $this->invoiceRepository->find($id); + return new InvoiceResource($invoice); + } catch (\Exception $e) { + Log::error('Error updating invoice: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'invoice_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour de la facture.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified invoice. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->invoiceRepository->delete($id); + + if (! $deleted) { + return response()->json([ + 'message' => 'Facture non trouvée ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Facture supprimée avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting invoice: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'invoice_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression de la facture.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Create an invoice from a quote. + */ + public function createFromQuote(string $quoteId): InvoiceResource|JsonResponse + { + try { + $invoice = $this->invoiceRepository->createFromQuote($quoteId); + return new InvoiceResource($invoice); + } catch (\Exception $e) { + Log::error('Error creating invoice from quote: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'quote_id' => $quoteId, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création de la facture depuis le devis.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Send the invoice by email to the client. + */ + public function sendByEmail(string $id): JsonResponse + { + try { + $invoice = $this->invoiceRepository->find($id); + + if (!$invoice) { + return response()->json(['message' => 'Facture non trouvée.'], 404); + } + + if (!$invoice->client || !$invoice->client->email) { + return response()->json(['message' => 'Le client n\'a pas d\'adresse email.'], 422); + } + + // Load lines to ensure they are available in the view + $invoice->load('lines'); + + // Generate PDF + $pdfContent = Pdf::loadView('pdf.invoice_pdf', ['invoice' => $invoice])->output(); + + // Send Email + Mail::to($invoice->client->email)->send(new DocumentMail($invoice, 'invoice', $pdfContent)); + + return response()->json([ + 'message' => 'La facture a été envoyée avec succès à ' . $invoice->client->email, + ], 200); + } catch (\Exception $e) { + Log::error('Error sending invoice email: ' . $e->getMessage(), [ + 'exception' => $e, + 'invoice_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de l\'envoi de l\'email.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/PractitionerDocumentController.php b/thanasoft-back/app/Http/Controllers/Api/PractitionerDocumentController.php new file mode 100644 index 0000000..eb0e1f3 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/PractitionerDocumentController.php @@ -0,0 +1,320 @@ + $request->get('search'), + 'practitioner_id' => $request->get('practitioner_id'), + 'doc_type' => $request->get('doc_type'), + 'valid_only' => $request->get('valid_only'), + 'sort_by' => $request->get('sort_by', 'created_at'), + 'sort_direction' => $request->get('sort_direction', 'desc'), + ]; + + // Remove null filters + $filters = array_filter($filters, function ($value) { + return $value !== null && $value !== ''; + }); + + $documents = $this->practitionerDocumentRepository->getAll($filters); + + return new PractitionerDocumentCollection($documents); + + } catch (\Exception $e) { + Log::error('Error fetching practitioner documents: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des documents des praticiens.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display paginated practitioner documents. + */ + public function paginated(Request $request): JsonResponse + { + try { + $perPage = $request->get('per_page', 15); + $result = $this->practitionerDocumentRepository->getPaginated($perPage); + + return response()->json([ + 'data' => new PractitionerDocumentCollection($result['documents']), + 'pagination' => $result['pagination'], + 'message' => 'Documents des praticiens récupérés avec succès.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error fetching paginated practitioner documents: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des documents des praticiens.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get documents by practitioner ID. + */ + public function byPractitioner(string $practitionerId): PractitionerDocumentCollection|JsonResponse + { + try { + $documents = $this->practitionerDocumentRepository->getByPractitionerId((int) $practitionerId); + return new PractitionerDocumentCollection($documents); + } catch (\Exception $e) { + Log::error('Error fetching documents by practitioner: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'practitioner_id' => $practitionerId, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des documents du praticien.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get documents by type. + */ + public function byType(Request $request): PractitionerDocumentCollection|JsonResponse + { + try { + $docType = $request->get('doc_type'); + + if (!$docType) { + return response()->json([ + 'message' => 'Le paramètre doc_type est requis.', + ], 400); + } + + $documents = $this->practitionerDocumentRepository->getByDocumentType($docType); + return new PractitionerDocumentCollection($documents); + } catch (\Exception $e) { + Log::error('Error fetching documents by type: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'doc_type' => $docType, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des documents par type.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get valid documents (not expired). + */ + public function valid(): PractitionerDocumentCollection|JsonResponse + { + try { + $documents = $this->practitionerDocumentRepository->getValid(); + return new PractitionerDocumentCollection($documents); + } catch (\Exception $e) { + Log::error('Error fetching valid documents: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des documents valides.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get expired documents. + */ + public function expired(): PractitionerDocumentCollection|JsonResponse + { + try { + $documents = $this->practitionerDocumentRepository->getExpired(); + return new PractitionerDocumentCollection($documents); + } catch (\Exception $e) { + Log::error('Error fetching expired documents: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des documents expirés.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get practitioner document statistics. + */ + public function statistics(): JsonResponse + { + try { + $statistics = $this->practitionerDocumentRepository->getStatistics(); + + return response()->json([ + 'data' => $statistics, + 'message' => 'Statistiques des documents des praticiens récupérées avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error fetching practitioner document statistics: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des statistiques.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created practitioner document. + */ + public function store(StorePractitionerDocumentRequest $request): PractitionerDocumentResource|JsonResponse + { + try { + $document = $this->practitionerDocumentRepository->create($request->validated()); + return new PractitionerDocumentResource($document); + } catch (\Exception $e) { + Log::error('Error creating practitioner document: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du document du praticien.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified practitioner document. + */ + public function show(string $id): PractitionerDocumentResource|JsonResponse + { + try { + $document = $this->practitionerDocumentRepository->find($id); + + if (!$document) { + return response()->json([ + 'message' => 'Document du praticien non trouvé.', + ], 404); + } + + return new PractitionerDocumentResource($document); + } catch (\Exception $e) { + Log::error('Error fetching practitioner document: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'document_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du document du praticien.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified practitioner document. + */ + public function update(UpdatePractitionerDocumentRequest $request, string $id): PractitionerDocumentResource|JsonResponse + { + try { + $updated = $this->practitionerDocumentRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Document du praticien non trouvé ou échec de la mise à jour.', + ], 404); + } + + $document = $this->practitionerDocumentRepository->find($id); + return new PractitionerDocumentResource($document); + } catch (\Exception $e) { + Log::error('Error updating practitioner document: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'document_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du document du praticien.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified practitioner document. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->practitionerDocumentRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Document du praticien non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Document du praticien supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting practitioner document: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'document_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du document du praticien.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/PriceListController.php b/thanasoft-back/app/Http/Controllers/Api/PriceListController.php new file mode 100644 index 0000000..73c6f41 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/PriceListController.php @@ -0,0 +1,154 @@ +priceListRepository->all()->sortBy('name')->values(); + + return PriceListResource::collection($priceLists); + } catch (\Exception $e) { + Log::error('Error fetching price lists: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des listes de prix.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created price list. + */ + public function store(StorePriceListRequest $request): PriceListResource|JsonResponse + { + try { + $priceList = $this->priceListRepository->create($request->validated()); + + return new PriceListResource($priceList); + } catch (\Exception $e) { + Log::error('Error creating price list: ' . $e->getMessage(), [ + 'exception' => $e, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création de la liste de prix.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified price list. + */ + public function show(string $id): PriceListResource|JsonResponse + { + try { + $priceList = $this->priceListRepository->find($id); + + if (! $priceList) { + return response()->json([ + 'message' => 'Liste de prix non trouvée.', + ], 404); + } + + return new PriceListResource($priceList); + } catch (\Exception $e) { + Log::error('Error fetching price list: ' . $e->getMessage(), [ + 'exception' => $e, + 'price_list_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération de la liste de prix.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified price list. + */ + public function update(UpdatePriceListRequest $request, string $id): PriceListResource|JsonResponse + { + try { + $updated = $this->priceListRepository->update($id, $request->validated()); + + if (! $updated) { + return response()->json([ + 'message' => 'Liste de prix non trouvée ou échec de la mise à jour.', + ], 404); + } + + $priceList = $this->priceListRepository->find($id); + + return new PriceListResource($priceList); + } catch (\Exception $e) { + Log::error('Error updating price list: ' . $e->getMessage(), [ + 'exception' => $e, + 'price_list_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour de la liste de prix.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified price list. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->priceListRepository->delete($id); + + if (! $deleted) { + return response()->json([ + 'message' => 'Liste de prix non trouvée ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Liste de prix supprimée avec succès.', + ]); + } catch (\Exception $e) { + Log::error('Error deleting price list: ' . $e->getMessage(), [ + 'exception' => $e, + 'price_list_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression de la liste de prix.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/ProductCategoryController.php b/thanasoft-back/app/Http/Controllers/Api/ProductCategoryController.php new file mode 100644 index 0000000..4facd09 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/ProductCategoryController.php @@ -0,0 +1,349 @@ +get('per_page', 15); + $filters = [ + 'search' => $request->get('search'), + 'active' => $request->get('active'), + 'parent_id' => $request->get('parent_id'), + 'sort_by' => $request->get('sort_by', 'name'), + 'sort_direction' => $request->get('sort_direction', 'asc'), + ]; + + // Remove null filters + $filters = array_filter($filters, function ($value) { + return $value !== null && $value !== ''; + }); + + $categories = $this->productCategoryRepository->paginate($perPage, $filters); + + return new ProductCategoryCollection($categories); + + } catch (\Exception $e) { + Log::error('Error fetching product categories: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des catégories de produits.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created product category. + */ + public function store(StoreProductCategoryRequest $request): ProductCategoryResource|JsonResponse + { + try { + $category = $this->productCategoryRepository->create($request->validated()); + return new ProductCategoryResource($category); + } catch (\Exception $e) { + Log::error('Error creating product category: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création de la catégorie.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified product category. + */ + public function show(string $id): ProductCategoryResource|JsonResponse + { + try { + $category = $this->productCategoryRepository->find($id); + + if (!$category) { + return response()->json([ + 'message' => 'Catégorie non trouvée.', + ], 404); + } + + return new ProductCategoryResource($category); + } catch (\Exception $e) { + Log::error('Error fetching product category: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'category_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération de la catégorie.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified product category. + */ + public function update(UpdateProductCategoryRequest $request, string $id): ProductCategoryResource|JsonResponse + { + try { + $updated = $this->productCategoryRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Catégorie non trouvée ou échec de la mise à jour.', + ], 404); + } + + $category = $this->productCategoryRepository->find($id); + return new ProductCategoryResource($category); + } catch (\Exception $e) { + Log::error('Error updating product category: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'category_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour de la catégorie.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified product category. + */ + public function destroy(string $id): JsonResponse + { + try { + // Check if category can be deleted + if (!$this->productCategoryRepository->canDelete($id)) { + return response()->json([ + 'message' => 'Impossible de supprimer cette catégorie. Elle peut avoir des sous-catégories ou des produits associés.', + ], 422); + } + + $deleted = $this->productCategoryRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Catégorie non trouvée ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Catégorie supprimée avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting product category: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'category_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression de la catégorie.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get active categories only + */ + public function active(): ProductCategoryCollection|JsonResponse + { + try { + $categories = $this->productCategoryRepository->getActive(); + return new ProductCategoryCollection(collect(['data' => $categories])); + } catch (\Exception $e) { + Log::error('Error fetching active product categories: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des catégories actives.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get root categories (no parent) + */ + public function roots(): ProductCategoryCollection|JsonResponse + { + try { + $categories = $this->productCategoryRepository->getRoots(); + return new ProductCategoryCollection(collect(['data' => $categories])); + } catch (\Exception $e) { + Log::error('Error fetching root product categories: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des catégories racine.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get categories with their children (hierarchical structure) + */ + public function hierarchical(): ProductCategoryCollection|JsonResponse + { + try { + $categories = $this->productCategoryRepository->getWithChildren(); + return new ProductCategoryCollection(collect(['data' => $categories])); + } catch (\Exception $e) { + Log::error('Error fetching hierarchical product categories: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération de la structure hiérarchique.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Search categories by name, code or description + */ + public function search(Request $request): ProductCategoryCollection|JsonResponse + { + try { + $term = $request->get('term', ''); + $perPage = (int) $request->get('per_page', 15); + + if (empty($term)) { + return response()->json([ + 'message' => 'Le paramètre "term" est requis pour la recherche.', + ], 400); + } + + $categories = $this->productCategoryRepository->search($term, $perPage); + + return new ProductCategoryCollection($categories); + + } catch (\Exception $e) { + Log::error('Error searching product categories: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'search_term' => $term, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recherche des catégories.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get category statistics + */ + public function statistics(): JsonResponse + { + try { + $stats = $this->productCategoryRepository->getStatistics(); + + return response()->json([ + 'data' => $stats, + 'message' => 'Statistiques des catégories récupérées avec succès.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error fetching product category statistics: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des statistiques.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Toggle category active status + */ + public function toggleActive(Request $request, string $id): ProductCategoryResource|JsonResponse + { + try { + $request->validate([ + 'active' => 'required|boolean', + ]); + + $category = $this->productCategoryRepository->find($id); + + if (!$category) { + return response()->json([ + 'message' => 'Catégorie non trouvée.', + ], 404); + } + + $updated = $this->productCategoryRepository->update($id, [ + 'active' => $request->boolean('active') + ]); + + if (!$updated) { + return response()->json([ + 'message' => 'Échec de la mise à jour du statut.', + ], 422); + } + + $category = $this->productCategoryRepository->find($id); + return new ProductCategoryResource($category); + + } catch (\Exception $e) { + Log::error('Error toggling product category status: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'category_id' => $id, + 'data' => $request->all(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du statut.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/ProductController.php b/thanasoft-back/app/Http/Controllers/Api/ProductController.php new file mode 100644 index 0000000..33bec79 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/ProductController.php @@ -0,0 +1,376 @@ +get('per_page', 15); + $filters = [ + 'search' => $request->get('search'), + 'categorie' => $request->get('categorie_id'), + 'fournisseur_id' => $request->get('fournisseur_id'), + 'low_stock' => $request->get('low_stock'), + 'expiring_soon' => $request->get('expiring_soon'), + 'is_intervention' => $request->get('is_intervention'), + 'sort_by' => $request->get('sort_by', 'created_at'), + 'sort_direction' => $request->get('sort_direction', 'desc'), + ]; + + // Remove null filters + $filters = array_filter($filters, function ($value) { + return $value !== null && $value !== ''; + }); + + $products = $this->productRepository->paginate($perPage, $filters); + + return new ProductCollection($products); + + } catch (\Exception $e) { + Log::error('Error fetching products: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des produits.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created product. + */ + public function store(StoreProductRequest $request): ProductResource|JsonResponse + { + try { + $validatedData = $request->validated(); + + // Handle image upload + if ($request->hasFile('image')) { + // Create product without image first + $product = $this->productRepository->create($validatedData); + + // Upload and attach image + $imagePath = $product->uploadImage($request->file('image')); + + // Refresh product to get updated data + $product = $this->productRepository->find($product->id); + } else { + $product = $this->productRepository->create($validatedData); + } + + return new ProductResource($product); + } catch (\Exception $e) { + Log::error('Error creating product: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du produit.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified product. + */ + public function show(string $id): ProductResource|JsonResponse + { + try { + $product = $this->productRepository->find($id); + + if (!$product) { + return response()->json([ + 'message' => 'Produit non trouvé.', + ], 404); + } + + return new ProductResource($product); + } catch (\Exception $e) { + Log::error('Error fetching product: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'product_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du produit.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Search products by name. + */ + public function searchBy(Request $request): JsonResponse + { + try { + $name = $request->get('name', ''); + $exact = $request->boolean('exact', false); + + if (empty($name)) { + return response()->json([ + 'message' => 'Le paramètre "name" est requis.', + ], 400); + } + + $products = $this->productRepository->searchByName($name, 15, $exact); + + return response()->json([ + 'data' => $products, + 'count' => $products->count(), + 'message' => $products->count() > 0 + ? 'Produits trouvés avec succès.' + : 'Aucun produit trouvé.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error searching products by name: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'search_term' => $name, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recherche des produits.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get products with low stock. + */ + public function lowStock(Request $request): ProductCollection|JsonResponse + { + try { + $perPage = (int) $request->get('per_page', 15); + $products = $this->productRepository->getLowStockProducts($perPage); + + return new ProductCollection($products); + + } catch (\Exception $e) { + Log::error('Error fetching low stock products: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des produits à stock faible.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get products by category. + */ + public function byCategory(Request $request): ProductCollection|JsonResponse + { + try { + $categoryId = $request->get('category_id'); + $perPage = (int) $request->get('per_page', 15); + + if (empty($categoryId)) { + return response()->json([ + 'message' => 'Le paramètre "category_id" est requis.', + ], 400); + } + + $products = $this->productRepository->getByCategory($categoryId, $perPage); + + return new ProductCollection($products); + + } catch (\Exception $e) { + Log::error('Error fetching products by category: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'category_id' => $categoryId, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des produits par catégorie.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get products statistics. + */ + public function statistics(): JsonResponse + { + try { + $stats = $this->productRepository->getStatistics(); + + return response()->json([ + 'data' => $stats, + 'message' => 'Statistiques des produits récupérées avec succès.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error fetching product statistics: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des statistiques.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified product. + */ + public function update(UpdateProductRequest $request, string $id): ProductResource|JsonResponse + { + try { + $validatedData = $request->validated(); + $product = $this->productRepository->find($id); + + if (!$product) { + return response()->json([ + 'message' => 'Produit non trouvé.', + ], 404); + } + + // Handle image upload/removal + if ($request->boolean('remove_image')) { + // Remove existing image + $product->deleteImage(); + } elseif ($request->hasFile('image')) { + // Upload new image + $product->uploadImage($request->file('image')); + } + + // Remove image-related fields from validated data before updating other fields + unset($validatedData['image'], $validatedData['remove_image']); + + // Update other product fields + $updated = $this->productRepository->update($id, $validatedData); + + if (!$updated) { + return response()->json([ + 'message' => 'Produit non trouvé ou échec de la mise à jour.', + ], 404); + } + + $product = $this->productRepository->find($id); + return new ProductResource($product); + } catch (\Exception $e) { + Log::error('Error updating product: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'product_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du produit.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified product. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->productRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Produit non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Produit supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting product: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'product_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du produit.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update stock quantity for a product. + */ + public function updateStock(Request $request, string $id): JsonResponse + { + try { + $request->validate([ + 'stock_actuel' => 'required|numeric|min:0', + ]); + + $updated = $this->productRepository->updateStock((int) $id, $request->stock_actuel); + + if (!$updated) { + return response()->json([ + 'message' => 'Produit non trouvé ou échec de la mise à jour du stock.', + ], 404); + } + + $product = $this->productRepository->find($id); + + return response()->json([ + 'data' => new ProductResource($product), + 'message' => 'Stock mis à jour avec succès.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error updating product stock: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'product_id' => $id, + 'stock_data' => $request->all(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du stock.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/PurchaseOrderController.php b/thanasoft-back/app/Http/Controllers/Api/PurchaseOrderController.php new file mode 100644 index 0000000..37ffa6d --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/PurchaseOrderController.php @@ -0,0 +1,226 @@ +purchaseOrderRepository->all(); + return PurchaseOrderResource::collection($purchaseOrders); + } + catch (\Exception $e) { + Log::error('Error fetching purchase orders: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des commandes fournisseurs.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created purchase order. + */ + public function store(StorePurchaseOrderRequest $request): PurchaseOrderResource|JsonResponse + { + try { + $purchaseOrder = $this->purchaseOrderRepository->create($request->validated()); + + // If PO is created directly as validated/delivered, ensure a draft goods receipt exists. + if ($purchaseOrder && in_array($purchaseOrder->status, ['confirmee', 'livree'], true)) { + $this->createGoodsReceiptFromValidatedPurchaseOrder($purchaseOrder); + } + + return new PurchaseOrderResource($purchaseOrder); + } + catch (\Exception $e) { + Log::error('Error creating purchase order: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création de la commande fournisseur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified purchase order. + */ + public function show(string $id): PurchaseOrderResource|JsonResponse + { + try { + $purchaseOrder = $this->purchaseOrderRepository->find($id); + + if (!$purchaseOrder) { + return response()->json([ + 'message' => 'Commande fournisseur non trouvée.', + ], 404); + } + + return new PurchaseOrderResource($purchaseOrder); + } + catch (\Exception $e) { + Log::error('Error fetching purchase order: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'purchase_order_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération de la commande fournisseur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified purchase order. + */ + public function update(UpdatePurchaseOrderRequest $request, string $id): PurchaseOrderResource|JsonResponse + { + try { + $updated = $this->purchaseOrderRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Commande fournisseur non trouvée ou échec de la mise à jour.', + ], 404); + } + + $purchaseOrder = $this->purchaseOrderRepository->find($id); + + // Ensure draft goods receipt exists when PO is validated/delivered. + // Idempotent: guarded by purchase_order_id existence check in helper. + if ($purchaseOrder && in_array($purchaseOrder->status, ['confirmee', 'livree'], true)) { + $this->createGoodsReceiptFromValidatedPurchaseOrder($purchaseOrder); + } + + return new PurchaseOrderResource($purchaseOrder); + } + catch (\Exception $e) { + Log::error('Error updating purchase order: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'purchase_order_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour de la commande fournisseur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Create a draft goods receipt when a purchase order is validated. + */ + protected function createGoodsReceiptFromValidatedPurchaseOrder($purchaseOrder): void + { + $alreadyExists = GoodsReceipt::query() + ->where('purchase_order_id', $purchaseOrder->id) + ->exists(); + + if ($alreadyExists) { + return; + } + + $warehouseId = Warehouse::query()->value('id'); + if (!$warehouseId) { + throw new \RuntimeException('Aucun entrepôt disponible pour créer la réception de marchandise.'); + } + + $receiptNumber = 'GR-' . now()->format('Ym') . '-' . str_pad((string)$purchaseOrder->id, 4, '0', STR_PAD_LEFT); + + $lines = collect($purchaseOrder->lines ?? []) + ->filter(fn($line) => !empty($line->product_id)) + ->map(function ($line) { + return [ + 'product_id' => (int)$line->product_id, + 'packaging_id' => null, + 'packages_qty_received' => null, + 'units_qty_received' => (float)$line->quantity, + 'qty_received_base' => (float)$line->quantity, + 'unit_price' => (float)$line->unit_price, + 'unit_price_per_package' => null, + 'tva_rate_id' => null, + ]; + }) + ->values() + ->all(); + + $this->goodsReceiptRepository->create([ + 'purchase_order_id' => $purchaseOrder->id, + 'warehouse_id' => (int)$warehouseId, + 'receipt_number' => $receiptNumber, + 'receipt_date' => now()->toDateString(), + 'status' => 'draft', + 'lines' => $lines, + ]); + } + + /** + * Remove the specified purchase order. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->purchaseOrderRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Commande fournisseur non trouvée ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Commande fournisseur supprimée avec succès.', + ], 200); + } + catch (\Exception $e) { + Log::error('Error deleting purchase order: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'purchase_order_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression de la commande fournisseur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Http/Controllers/Api/QuoteController.php b/thanasoft-back/app/Http/Controllers/Api/QuoteController.php new file mode 100644 index 0000000..c293ae1 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/QuoteController.php @@ -0,0 +1,233 @@ +quoteRepository->all(); + return QuoteResource::collection($quotes); + } catch (\Exception $e) { + Log::error('Error fetching quotes: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des devis.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created quote. + */ + public function store(StoreQuoteRequest $request): QuoteResource|JsonResponse + { + try { + $quote = $this->quoteRepository->create($request->validated()); + return new QuoteResource($quote); + } catch (\Exception $e) { + Log::error('Error creating quote: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du devis.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified quote. + */ + public function show(string $id): QuoteResource|JsonResponse + { + try { + $quote = $this->quoteRepository->find($id); + + if (! $quote) { + return response()->json([ + 'message' => 'Devis non trouvé.', + ], 404); + } + + return new QuoteResource($quote); + } catch (\Exception $e) { + Log::error('Error fetching quote: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'quote_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du devis.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified quote. + */ + public function update(UpdateQuoteRequest $request, string $id): QuoteResource|JsonResponse + { + try { + $updated = $this->quoteRepository->update($id, $request->validated()); + + if (! $updated) { + return response()->json([ + 'message' => 'Devis non trouvé ou échec de la mise à jour.', + ], 404); + } + + $quote = $this->quoteRepository->find($id); + return new QuoteResource($quote); + } catch (\Exception $e) { + Log::error('Error updating quote: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'quote_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du devis.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified quote. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->quoteRepository->delete($id); + + if (! $deleted) { + return response()->json([ + 'message' => 'Devis non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Devis supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting quote: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'quote_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du devis.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Send the quote by email to the client. + */ + public function sendByEmail(string $id): JsonResponse + { + try { + $quote = $this->quoteRepository->find($id); + + if (!$quote) { + return response()->json(['message' => 'Devis non trouvé.'], 404); + } + + if (!$quote->client || !$quote->client->email) { + return response()->json(['message' => 'Le client n\'a pas d\'adresse email.'], 422); + } + + // Load lines to ensure they are available in the view + $quote->load('lines'); + + // Generate PDF + $pdfContent = Pdf::loadView('pdf.quote_pdf', ['quote' => $quote])->output(); + + // Send Email + Mail::to($quote->client->email)->send(new DocumentMail($quote, 'quote', $pdfContent)); + + return response()->json([ + 'message' => 'Le devis a été envoyé avec succès à ' . $quote->client->email, + ], 200); + } catch (\Exception $e) { + Log::error('Error sending quote email: ' . $e->getMessage(), [ + 'exception' => $e, + 'quote_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de l\'envoi de l\'email.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Download the quote as a PDF. + */ + public function downloadPdf(string $id): Response|JsonResponse + { + try { + $quote = Quote::with(['client', 'group', 'lines'])->find($id); + + if (! $quote) { + return response()->json([ + 'message' => 'Devis non trouvé.', + ], 404); + } + + $pdf = Pdf::loadView('pdf.quote_pdf', ['quote' => $quote]); + + return $pdf->download('Devis_' . $quote->reference . '.pdf'); + } catch (\Exception $e) { + Log::error('Error downloading quote PDF: ' . $e->getMessage(), [ + 'exception' => $e, + 'quote_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la generation du PDF.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/StockItemController.php b/thanasoft-back/app/Http/Controllers/Api/StockItemController.php new file mode 100644 index 0000000..dc0ca61 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/StockItemController.php @@ -0,0 +1,134 @@ +stockItemRepository->all(); + return response()->json([ + 'data' => StockItemResource::collection($items), + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error fetching stock items: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des stocks.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created stock item. + */ + public function store(StoreStockItemRequest $request): JsonResponse + { + try { + $item = $this->stockItemRepository->create($request->validated()); + return response()->json([ + 'data' => new StockItemResource($item), + 'message' => 'Stock initialisé avec succès.', + 'status' => 'success' + ], 201); + } catch (\Exception $e) { + Log::error('Error creating stock item: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de l\'initialisation du stock.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified stock item. + */ + public function show(string $id): JsonResponse + { + try { + $item = $this->stockItemRepository->find((int) $id); + if (!$item) { + return response()->json(['message' => 'Stock non trouvé.'], 404); + } + return response()->json([ + 'data' => new StockItemResource($item), + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error fetching stock item: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du stock.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified stock item. + */ + public function update(UpdateStockItemRequest $request, string $id): JsonResponse + { + try { + $updated = $this->stockItemRepository->update((int) $id, $request->validated()); + if (!$updated) { + return response()->json(['message' => 'Stock non trouvé ou échec de la mise à jour.'], 404); + } + $item = $this->stockItemRepository->find((int) $id); + return response()->json([ + 'data' => new StockItemResource($item), + 'message' => 'Stock mis à jour avec succès.', + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error updating stock item: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du stock.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified stock item. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->stockItemRepository->delete((int) $id); + if (!$deleted) { + return response()->json(['message' => 'Stock non trouvé ou échec de la suppression.'], 404); + } + return response()->json([ + 'message' => 'Stock supprimé avec succès.', + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error deleting stock item: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du stock.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/StockMoveController.php b/thanasoft-back/app/Http/Controllers/Api/StockMoveController.php new file mode 100644 index 0000000..55fe5bf --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/StockMoveController.php @@ -0,0 +1,85 @@ +stockMoveRepository->all(); + return response()->json([ + 'data' => StockMoveResource::collection($moves), + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error fetching stock moves: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des mouvements de stock.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created stock move. + */ + public function store(StoreStockMoveRequest $request): JsonResponse + { + try { + $move = $this->stockMoveRepository->create($request->validated()); + return response()->json([ + 'data' => new StockMoveResource($move), + 'message' => 'Mouvement de stock enregistré avec succès.', + 'status' => 'success' + ], 201); + } catch (\Exception $e) { + Log::error('Error creating stock move: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de l\'enregistrement du mouvement de stock.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified stock move. + */ + public function show(string $id): JsonResponse + { + try { + $move = $this->stockMoveRepository->find((int) $id); + if (!$move) { + return response()->json(['message' => 'Mouvement de stock non trouvé.'], 404); + } + return response()->json([ + 'data' => new StockMoveResource($move), + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error fetching stock move: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du mouvement de stock.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/ThanatopractitionerController.php b/thanasoft-back/app/Http/Controllers/Api/ThanatopractitionerController.php new file mode 100644 index 0000000..00b5c9d --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/ThanatopractitionerController.php @@ -0,0 +1,357 @@ +get('per_page', 15); + + $filters = [ + 'search' => $request->get('search'), + 'valid_authorization' => $request->get('valid_authorization'), + 'sort_by' => $request->get('sort_by', 'created_at'), + 'sort_direction' => $request->get('sort_direction', 'desc'), + ]; + + // Remove null filters + $filters = array_filter($filters, function ($value) { + return $value !== null && $value !== ''; + }); + + $result = $this->thanatopractitionerRepository->getPaginated($perPage, $filters); + + return response()->json([ + 'data' => new ThanatopractitionerCollection($result['thanatopractitioners']), + 'pagination' => $result['pagination'], + 'message' => 'Thanatopractitioners récupérés avec succès.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error fetching thanatopractitioners: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des thanatopractitioners.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display paginated thanatopractitioners. + */ + public function paginated(Request $request): JsonResponse + { + try { + $perPage = (int) $request->get('per_page', 15); + $result = $this->thanatopractitionerRepository->getPaginated($perPage, []); + + return response()->json([ + 'data' => new ThanatopractitionerCollection($result['thanatopractitioners']), + 'pagination' => $result['pagination'], + 'message' => 'Thanatopractitioners récupérés avec succès.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error fetching paginated thanatopractitioners: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des thanatopractitioners.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get thanatopractitioners with valid authorization. + */ + public function withValidAuthorization(): ThanatopractitionerCollection|JsonResponse + { + try { + $thanatopractitioners = $this->thanatopractitionerRepository->getWithValidAuthorization(); + return new ThanatopractitionerCollection($thanatopractitioners); + } catch (\Exception $e) { + Log::error('Error fetching thanatopractitioners with valid authorization: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des thanatopractitioners avec autorisation valide.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get thanatopractitioners with expired authorization. + */ + public function withExpiredAuthorization(): ThanatopractitionerCollection|JsonResponse + { + try { + $thanatopractitioners = $this->thanatopractitionerRepository->getWithExpiredAuthorization(); + return new ThanatopractitionerCollection($thanatopractitioners); + } catch (\Exception $e) { + Log::error('Error fetching thanatopractitioners with expired authorization: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des thanatopractitioners avec autorisation expirée.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get thanatopractitioners with their complete data. + */ + public function withRelations(): ThanatopractitionerCollection|JsonResponse + { + try { + $thanatopractitioners = $this->thanatopractitionerRepository->getWithRelations(); + return new ThanatopractitionerCollection($thanatopractitioners); + } catch (\Exception $e) { + Log::error('Error fetching thanatopractitioners with relations: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des thanatopractitioners avec relations.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get thanatopractitioner statistics. + */ + public function statistics(): JsonResponse + { + try { + $statistics = $this->thanatopractitionerRepository->getStatistics(); + + return response()->json([ + 'data' => $statistics, + 'message' => 'Statistiques des thanatopractitioners récupérées avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error fetching thanatopractitioner statistics: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des statistiques.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created thanatopractitioner. + */ + public function store(StoreThanatopractitionerRequest $request): ThanatopractitionerResource|JsonResponse + { + try { + $thanatopractitioner = $this->thanatopractitionerRepository->create($request->validated()); + return new ThanatopractitionerResource($thanatopractitioner); + } catch (\Exception $e) { + Log::error('Error creating thanatopractitioner: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du thanatopractitioner.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified thanatopractitioner. + */ + public function show(string $id): ThanatopractitionerResource|JsonResponse + { + try { + $thanatopractitioner = $this->thanatopractitionerRepository->findById((int) $id); + + if (!$thanatopractitioner) { + return response()->json([ + 'message' => 'Thanatopractitioner non trouvé.', + ], 404); + } + + return new ThanatopractitionerResource($thanatopractitioner); + } catch (\Exception $e) { + Log::error('Error fetching thanatopractitioner: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'thanatopractitioner_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du thanatopractitioner.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Find a thanatopractitioner by employee ID. + */ + public function findByEmployee(string $employeeId): ThanatopractitionerResource|JsonResponse + { + try { + $thanatopractitioner = $this->thanatopractitionerRepository->findByEmployeeId((int) $employeeId); + + if (!$thanatopractitioner) { + return response()->json([ + 'message' => 'Thanatopractitioner non trouvé pour cet employé.', + ], 404); + } + + return new ThanatopractitionerResource($thanatopractitioner); + } catch (\Exception $e) { + Log::error('Error fetching thanatopractitioner by employee: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'employee_id' => $employeeId, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du thanatopractitioner.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Search thanatopractitioners by employee name. + */ + public function searchByEmployeeName(Request $request): JsonResponse + { + try { + $query = $request->get('query', ''); + + if (strlen($query) < 2) { + return response()->json([ + 'data' => [], + 'message' => 'Veuillez entrer au moins 2 caractères pour la recherche.', + ], 200); + } + + $thanatopractitioners = $this->thanatopractitionerRepository->searchByEmployeeName($query); + + return response()->json([ + 'data' => new ThanatopractitionerCollection($thanatopractitioners), + 'message' => 'Recherche effectuée avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error searching thanatopractitioners by employee name: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'query' => $request->get('query'), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recherche.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified thanatopractitioner. + */ + public function update(UpdateThanatopractitionerRequest $request, string $id): ThanatopractitionerResource|JsonResponse + { + try { + $updated = $this->thanatopractitionerRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Thanatopractitioner non trouvé ou échec de la mise à jour.', + ], 404); + } + + $thanatopractitioner = $this->thanatopractitionerRepository->find($id); + return new ThanatopractitionerResource($thanatopractitioner); + } catch (\Exception $e) { + Log::error('Error updating thanatopractitioner: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'thanatopractitioner_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du thanatopractitioner.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified thanatopractitioner. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->thanatopractitionerRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Thanatopractitioner non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Thanatopractitioner supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting thanatopractitioner: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'thanatopractitioner_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du thanatopractitioner.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/TvaRateController.php b/thanasoft-back/app/Http/Controllers/Api/TvaRateController.php new file mode 100644 index 0000000..b4c4d36 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/TvaRateController.php @@ -0,0 +1,133 @@ +tvaRateRepository->all(); + return response()->json([ + 'data' => TvaRateResource::collection($tvaRates), + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error fetching TVA rates: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des taux de TVA.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created TVA rate. + */ + public function store(StoreTvaRateRequest $request): JsonResponse + { + try { + $tvaRate = $this->tvaRateRepository->create($request->validated()); + return response()->json([ + 'data' => new TvaRateResource($tvaRate), + 'message' => 'Taux de TVA créé avec succès.', + 'status' => 'success' + ], 201); + } catch (\Exception $e) { + Log::error('Error creating TVA rate: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du taux de TVA.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified TVA rate. + */ + public function show(string $id): JsonResponse + { + try { + $tvaRate = $this->tvaRateRepository->find((int) $id); + if (!$tvaRate) { + return response()->json(['message' => 'Taux de TVA non trouvé.'], 404); + } + return response()->json([ + 'data' => new TvaRateResource($tvaRate), + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error fetching TVA rate: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du taux de TVA.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified TVA rate. + */ + public function update(UpdateTvaRateRequest $request, string $id): JsonResponse + { + try { + $updated = $this->tvaRateRepository->update((int) $id, $request->validated()); + if (!$updated) { + return response()->json(['message' => 'Taux de TVA non trouvé ou échec de la mise à jour.'], 404); + } + $tvaRate = $this->tvaRateRepository->find((int) $id); + return response()->json([ + 'data' => new TvaRateResource($tvaRate), + 'message' => 'Taux de TVA mis à jour avec succès.', + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error updating TVA rate: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du taux de TVA.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified TVA rate. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->tvaRateRepository->delete((int) $id); + if (!$deleted) { + return response()->json(['message' => 'Taux de TVA non trouvé ou échec de la suppression.'], 404); + } + return response()->json([ + 'message' => 'Taux de TVA supprimé avec succès.', + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error deleting TVA rate: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du taux de TVA.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/UserController.php b/thanasoft-back/app/Http/Controllers/Api/UserController.php new file mode 100644 index 0000000..b1f0899 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/UserController.php @@ -0,0 +1,178 @@ +query('email'); + + if ($email) { + $user = User::query()->with(['roles', 'permissions'])->where('email', $email)->first(); + + return response()->json([ + 'data' => $user, + 'message' => $user + ? 'Utilisateur recupere avec succes.' + : 'Aucun utilisateur trouve pour cet email.', + ]); + } + + return response()->json([ + 'data' => $this->userRepository->all()->load(['roles', 'permissions'])->sortBy('name')->values(), + 'message' => 'Utilisateurs recuperes avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error fetching users: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recuperation des utilisateurs.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function store(StoreUserRequest $request): JsonResponse + { + try { + $user = $this->userRepository->create($request->validated()); + + return response()->json([ + 'data' => $user->load('roles', 'permissions'), + 'message' => 'Utilisateur cree avec succes.', + ], 201); + } catch (\Exception $e) { + Log::error('Error creating user: ' . $e->getMessage(), [ + 'exception' => $e, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la creation de l\'utilisateur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function show(string $id): JsonResponse + { + try { + $user = $this->userRepository->find($id); + + if (! $user) { + return response()->json([ + 'message' => 'Utilisateur non trouve.', + ], 404); + } + + return response()->json([ + 'data' => $user->load('roles', 'permissions'), + 'message' => 'Utilisateur recupere avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error fetching user: ' . $e->getMessage(), [ + 'exception' => $e, + 'user_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recuperation de l\'utilisateur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function update(UpdateUserRequest $request, string $id): JsonResponse + { + try { + $user = $this->userRepository->find($id); + + if (! $user) { + return response()->json([ + 'message' => 'Utilisateur non trouve ou echec de la mise a jour.', + ], 404); + } + + $validated = $request->validated(); + $clearPassword = (bool) ($validated['clear_password'] ?? false); + + unset($validated['clear_password']); + + if ($clearPassword) { + $validated['password'] = null; + } elseif (empty($validated['password'])) { + unset($validated['password']); + } + + $updated = $this->userRepository->update($id, $validated); + + if (! $updated) { + return response()->json([ + 'message' => 'Utilisateur non trouve ou echec de la mise a jour.', + ], 404); + } + + return response()->json([ + 'data' => $user->fresh()->load('roles', 'permissions'), + 'message' => 'Utilisateur mis a jour avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error updating user: ' . $e->getMessage(), [ + 'exception' => $e, + 'user_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise a jour de l\'utilisateur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->userRepository->delete($id); + + if (! $deleted) { + return response()->json([ + 'message' => 'Utilisateur non trouve ou echec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Utilisateur supprime avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error deleting user: ' . $e->getMessage(), [ + 'exception' => $e, + 'user_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression de l\'utilisateur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/VehicleController.php b/thanasoft-back/app/Http/Controllers/Api/VehicleController.php new file mode 100644 index 0000000..ac24ddb --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/VehicleController.php @@ -0,0 +1,144 @@ +vehicleRepository->paginate( + (int) $request->integer('per_page', 15), + $request->only(['search', 'status', 'vehicle_type', 'sort_by', 'sort_direction']) + ); + + return response()->json([ + 'data' => VehicleResource::collection($vehicles->items()), + 'meta' => [ + 'current_page' => $vehicles->currentPage(), + 'last_page' => $vehicles->lastPage(), + 'per_page' => $vehicles->perPage(), + 'total' => $vehicles->total(), + ], + 'status' => 'success', + ]); + } catch (\Exception $e) { + Log::error('Error fetching vehicles: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while fetching vehicles.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function store(StoreVehicleRequest $request): JsonResponse + { + try { + $vehicle = $this->vehicleRepository->create($request->validated()); + + return response()->json([ + 'data' => new VehicleResource($vehicle->load('primaryUser')), + 'message' => 'Vehicle created successfully.', + 'status' => 'success', + ], 201); + } catch (\Exception $e) { + Log::error('Error creating vehicle: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while creating the vehicle.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function show(string $id): JsonResponse + { + try { + $vehicle = $this->vehicleRepository->find((int) $id); + + if (! $vehicle) { + return response()->json(['message' => 'Vehicle not found.'], 404); + } + + $vehicle->load(['primaryUser', 'convoys']); + + return response()->json([ + 'data' => new VehicleResource($vehicle), + 'status' => 'success', + ]); + } catch (\Exception $e) { + Log::error('Error fetching vehicle: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while fetching the vehicle.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function update(UpdateVehicleRequest $request, string $id): JsonResponse + { + try { + $updated = $this->vehicleRepository->update((int) $id, $request->validated()); + + if (! $updated) { + return response()->json(['message' => 'Vehicle not found or update failed.'], 404); + } + + $vehicle = $this->vehicleRepository->find((int) $id); + + return response()->json([ + 'data' => new VehicleResource($vehicle->load('primaryUser')), + 'message' => 'Vehicle updated successfully.', + 'status' => 'success', + ]); + } catch (\Exception $e) { + Log::error('Error updating vehicle: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while updating the vehicle.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->vehicleRepository->delete((int) $id); + + if (! $deleted) { + return response()->json(['message' => 'Vehicle not found or delete failed.'], 404); + } + + return response()->json([ + 'message' => 'Vehicle deleted successfully.', + 'status' => 'success', + ]); + } catch (\Exception $e) { + Log::error('Error deleting vehicle: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while deleting the vehicle.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/WarehouseController.php b/thanasoft-back/app/Http/Controllers/Api/WarehouseController.php new file mode 100644 index 0000000..e57f37b --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/WarehouseController.php @@ -0,0 +1,174 @@ +warehouseRepository->all(); + return response()->json([ + 'data' => WarehouseResource::collection($warehouses), + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error fetching warehouses: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des entrepôts.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created warehouse. + */ + public function store(StoreWarehouseRequest $request): JsonResponse + { + try { + $warehouse = $this->warehouseRepository->create($request->validated()); + return response()->json([ + 'data' => new WarehouseResource($warehouse), + 'message' => 'Entrepôt créé avec succès.', + 'status' => 'success' + ], 201); + } catch (\Exception $e) { + Log::error('Error creating warehouse: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création de l\'entrepôt.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display specified warehouse. + */ + public function show(string $id): JsonResponse + { + try { + $warehouse = $this->warehouseRepository->find((int) $id); + if (!$warehouse) { + return response()->json(['message' => 'Entrepôt non trouvé.'], 404); + } + return response()->json([ + 'data' => new WarehouseResource($warehouse), + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error fetching warehouse: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération de l\'entrepôt.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update specified warehouse. + */ + public function update(UpdateWarehouseRequest $request, string $id): JsonResponse + { + try { + $updated = $this->warehouseRepository->update((int) $id, $request->validated()); + if (!$updated) { + return response()->json(['message' => 'Entrepôt non trouvé ou échec de la mise à jour.'], 404); + } + $warehouse = $this->warehouseRepository->find((int) $id); + return response()->json([ + 'data' => new WarehouseResource($warehouse), + 'message' => 'Entrepôt mis à jour avec succès.', + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error updating warehouse: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour de l\'entrepôt.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove specified warehouse. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->warehouseRepository->delete((int) $id); + if (!$deleted) { + return response()->json(['message' => 'Entrepôt non trouvé ou échec de la suppression.'], 404); + } + return response()->json([ + 'message' => 'Entrepôt supprimé avec succès.', + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error deleting warehouse: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression de l\'entrepôt.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Search warehouses by name. + */ + public function searchBy(Request $request): JsonResponse + { + try { + $name = $request->query('name'); + $exactMatch = $request->query('exact_match', false); + + if (empty($name)) { + return response()->json([ + 'data' => [], + 'count' => 0, + 'message' => 'Le paramètre de recherche est requis.' + ], 400); + } + + $warehouses = $this->warehouseRepository->all(); + + $filtered = $warehouses->filter(function ($warehouse) use ($name, $exactMatch) { + if ($exactMatch) { + return strtolower($warehouse->name) === strtolower($name); + } + return stripos($warehouse->name, $name) !== false; + }); + + return response()->json([ + 'data' => WarehouseResource::collection($filtered), + 'count' => $filtered->count(), + 'message' => 'Recherche effectuée avec succès.' + ]); + } catch (\Exception $e) { + Log::error('Error searching warehouses: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recherche des entrepôts.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/WebmailController.php b/thanasoft-back/app/Http/Controllers/Api/WebmailController.php new file mode 100644 index 0000000..f9a378b --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/WebmailController.php @@ -0,0 +1,470 @@ +json([ + 'message' => 'Utilisateur non authentifie.', + ], 401); + } + + $messages = $this->webmailRepository->paginateForUser( + (int) $user->id, + [ + 'folder' => $request->query('folder'), + 'status' => $request->query('status'), + 'search' => $request->query('search'), + 'unread' => $request->has('unread') ? $request->boolean('unread') : null, + 'starred' => $request->has('starred') ? $request->boolean('starred') : null, + ], + max(1, (int) $request->integer('per_page', 15)), + ); + + return response()->json([ + 'data' => $messages->getCollection() + ->map(fn (WebmailMessage $message): array => (new WebmailMessageResource($message))->resolve()) + ->values(), + 'meta' => [ + 'current_page' => $messages->currentPage(), + 'last_page' => $messages->lastPage(), + 'per_page' => $messages->perPage(), + 'total' => $messages->total(), + ], + 'message' => 'Messages recuperes avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error fetching webmail messages: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recuperation des messages.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function stats(): JsonResponse + { + try { + $user = Auth::user(); + + if (! $user) { + return response()->json([ + 'message' => 'Utilisateur non authentifie.', + ], 401); + } + + return response()->json([ + 'data' => $this->webmailRepository->statsForUser((int) $user->id), + 'message' => 'Statistiques webmail recuperees avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error fetching webmail stats: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recuperation des statistiques webmail.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function show(string $id): JsonResponse + { + try { + $user = Auth::user(); + + if (! $user) { + return response()->json([ + 'message' => 'Utilisateur non authentifie.', + ], 401); + } + + $message = $this->webmailRepository->findForUser($id, (int) $user->id); + + if (! $message) { + return response()->json([ + 'message' => 'Message non trouve.', + ], 404); + } + + return response()->json([ + 'data' => (new WebmailMessageResource($message))->resolve(), + 'message' => 'Message recupere avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error fetching webmail message: ' . $e->getMessage(), [ + 'exception' => $e, + 'message_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recuperation du message.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function send(SendWebmailMessageRequest $request): JsonResponse + { + try { + $user = Auth::user(); + + if (! $user) { + return response()->json([ + 'message' => 'Utilisateur non authentifie.', + ], 401); + } + + $message = $this->webmailService->send($request->validated(), $user); + + return response()->json([ + 'data' => (new WebmailMessageResource($message))->resolve(), + 'message' => 'Email envoye avec succes.', + ], 201); + } catch (\Exception $e) { + Log::error('Error sending webmail message: ' . $e->getMessage(), [ + 'exception' => $e, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de l\'envoi de l\'email.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function receive(ReceiveWebmailMessageRequest $request): JsonResponse + { + try { + $user = Auth::user(); + + if (! $user) { + return response()->json([ + 'message' => 'Utilisateur non authentifie.', + ], 401); + } + + $message = $this->webmailService->receive($request->validated(), $user); + + return response()->json([ + 'data' => (new WebmailMessageResource($message))->resolve(), + 'message' => 'Email recu et enregistre avec succes.', + ], 201); + } catch (\Exception $e) { + Log::error('Error receiving webmail message: ' . $e->getMessage(), [ + 'exception' => $e, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de l\'enregistrement du message recu.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function syncMailtrap(Request $request): JsonResponse + { + try { + $user = Auth::user(); + + if (! $user) { + return response()->json([ + 'message' => 'Utilisateur non authentifie.', + ], 401); + } + + $result = $this->webmailService->syncMailtrapInbox( + $user, + max(1, min(50, (int) $request->integer('limit', 30))) + ); + + return response()->json([ + 'data' => $result, + 'message' => 'Synchronisation Mailtrap terminee avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error syncing Mailtrap webmail messages: ' . $e->getMessage(), [ + 'exception' => $e, + 'user_id' => Auth::id(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la synchronisation Mailtrap.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function sync(Request $request): JsonResponse + { + try { + $user = Auth::user(); + + if (! $user) { + return response()->json([ + 'message' => 'Utilisateur non authentifie.', + ], 401); + } + + $result = $this->webmailService->syncMailbox( + $user->loadMissing('mailboxSetting'), + max(1, min(50, (int) $request->integer('limit', 30))) + ); + + return response()->json([ + 'data' => $result, + 'message' => 'Synchronisation de la boite mail terminee avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error syncing mailbox webmail messages: ' . $e->getMessage(), [ + 'exception' => $e, + 'user_id' => Auth::id(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la synchronisation de la boite mail.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function testSmtp(): JsonResponse + { + try { + $user = Auth::user(); + + if (! $user) { + return response()->json([ + 'message' => 'Utilisateur non authentifie.', + ], 401); + } + + $result = $this->webmailService->testSmtp($user->loadMissing('mailboxSetting')); + + return response()->json([ + 'data' => $result, + 'message' => 'Test SMTP envoye avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error testing mailbox SMTP: ' . $e->getMessage(), [ + 'exception' => $e, + 'user_id' => Auth::id(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors du test SMTP.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function mailboxSettings(): JsonResponse + { + $user = Auth::user(); + + if (! $user) { + return response()->json([ + 'message' => 'Utilisateur non authentifie.', + ], 401); + } + + return response()->json([ + 'data' => $user->relationLoaded('mailboxSetting') + ? ($user->mailboxSetting ? (new UserMailboxSettingResource($user->mailboxSetting))->resolve() : null) + : ($user->mailboxSetting ? (new UserMailboxSettingResource($user->mailboxSetting))->resolve() : null), + 'message' => 'Configuration mailbox recuperee avec succes.', + ]); + } + + public function upsertMailboxSettings(UpsertUserMailboxSettingRequest $request): JsonResponse + { + try { + $user = Auth::user(); + + if (! $user) { + return response()->json([ + 'message' => 'Utilisateur non authentifie.', + ], 401); + } + + $validated = $request->validated(); + $clearImapPassword = (bool) ($validated['clear_imap_password'] ?? false); + $clearSmtpPassword = (bool) ($validated['clear_smtp_password'] ?? false); + + unset($validated['clear_imap_password'], $validated['clear_smtp_password']); + + if ($clearImapPassword) { + $validated['imap_password'] = null; + } elseif (array_key_exists('imap_password', $validated) && $validated['imap_password'] === null) { + unset($validated['imap_password']); + } + + if ($clearSmtpPassword) { + $validated['smtp_password'] = null; + } elseif (array_key_exists('smtp_password', $validated) && $validated['smtp_password'] === null) { + unset($validated['smtp_password']); + } + + /** @var UserMailboxSetting $settings */ + $settings = UserMailboxSetting::query()->updateOrCreate( + ['user_id' => $user->id], + $validated, + ); + + return response()->json([ + 'data' => (new UserMailboxSettingResource($settings))->resolve(), + 'message' => 'Configuration mailbox mise a jour avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error updating mailbox settings: ' . $e->getMessage(), [ + 'exception' => $e, + 'user_id' => Auth::id(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise a jour de la configuration mailbox.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function update(UpdateWebmailMessageRequest $request, string $id): JsonResponse + { + try { + $user = Auth::user(); + + if (! $user) { + return response()->json([ + 'message' => 'Utilisateur non authentifie.', + ], 401); + } + + $message = $this->webmailRepository->findForUser($id, (int) $user->id); + + if (! $message) { + return response()->json([ + 'message' => 'Message non trouve.', + ], 404); + } + + $validated = $request->validated(); + + if (array_key_exists('is_read', $validated)) { + $validated['read_at'] = $validated['is_read'] ? now() : null; + unset($validated['is_read']); + } + + if (array_key_exists('is_starred', $validated)) { + $validated['starred_at'] = $validated['is_starred'] ? now() : null; + unset($validated['is_starred']); + } + + $updated = $this->webmailRepository->update($id, $validated); + + if (! $updated) { + return response()->json([ + 'message' => 'Echec de la mise a jour du message.', + ], 422); + } + + /** @var WebmailMessage $freshMessage */ + $freshMessage = $this->webmailRepository->findForUser($id, (int) $user->id); + + return response()->json([ + 'data' => (new WebmailMessageResource($freshMessage))->resolve(), + 'message' => 'Message mis a jour avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error updating webmail message: ' . $e->getMessage(), [ + 'exception' => $e, + 'message_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise a jour du message.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function destroy(string $id): JsonResponse + { + try { + $user = Auth::user(); + + if (! $user) { + return response()->json([ + 'message' => 'Utilisateur non authentifie.', + ], 401); + } + + $message = $this->webmailRepository->findForUser($id, (int) $user->id); + + if (! $message) { + return response()->json([ + 'message' => 'Message non trouve.', + ], 404); + } + + $deleted = $this->webmailRepository->delete($id); + + if (! $deleted) { + return response()->json([ + 'message' => 'Echec de la suppression du message.', + ], 422); + } + + return response()->json([ + 'message' => 'Message supprime avec succes.', + ]); + } catch (\Exception $e) { + Log::error('Error deleting webmail message: ' . $e->getMessage(), [ + 'exception' => $e, + 'message_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du message.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Http/Requests/AssignClientsToGroupRequest.php b/thanasoft-back/app/Http/Requests/AssignClientsToGroupRequest.php new file mode 100644 index 0000000..f8fc207 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/AssignClientsToGroupRequest.php @@ -0,0 +1,42 @@ +|string> + */ + public function rules(): array + { + return [ + 'client_ids' => 'required|array|min:1', + 'client_ids.*' => 'integer|distinct|exists:clients,id', + ]; + } + + public function messages(): array + { + return [ + 'client_ids.required' => 'La liste des clients est obligatoire.', + 'client_ids.array' => 'La liste des clients doit être un tableau.', + 'client_ids.min' => 'Veuillez sélectionner au moins un client.', + 'client_ids.*.integer' => 'Chaque ID client doit être un entier.', + 'client_ids.*.distinct' => 'Un client ne peut pas être envoyé plusieurs fois.', + 'client_ids.*.exists' => 'Un ou plusieurs clients sélectionnés sont introuvables.', + ]; + } +} + diff --git a/thanasoft-back/app/Http/Requests/ClientCategoryRequest.php b/thanasoft-back/app/Http/Requests/ClientCategoryRequest.php new file mode 100644 index 0000000..f954903 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/ClientCategoryRequest.php @@ -0,0 +1,70 @@ +|string> + */ + public function rules(): array + { + $categoryId = $this->route('client_category')?->id; + + return [ + 'name' => [ + 'required', + 'string', + 'max:255', + Rule::unique('client_categories')->ignore($categoryId) + ], + 'slug' => [ + 'nullable', + 'string', + 'max:255', + 'alpha_dash', + Rule::unique('client_categories')->ignore($categoryId) + ], + 'description' => 'nullable|string|max:1000', + 'is_active' => 'boolean', + 'sort_order' => 'nullable|integer|min:0', + ]; + } + + public function attributes(): array + { + return [ + 'name' => 'category name', + 'slug' => 'URL slug', + ]; + } + + protected function prepareForValidation(): void + { + // Generate slug from name if not provided and name exists + if (!$this->slug && $this->name) { + $this->merge([ + 'slug' => \Str::slug($this->name), + ]); + } + + // Ensure boolean values are properly cast + if ($this->has('is_active')) { + $this->merge([ + 'is_active' => filter_var($this->is_active, FILTER_VALIDATE_BOOLEAN), + ]); + } + } +} diff --git a/thanasoft-back/app/Http/Requests/ReceiveWebmailMessageRequest.php b/thanasoft-back/app/Http/Requests/ReceiveWebmailMessageRequest.php new file mode 100644 index 0000000..8ffc9b9 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/ReceiveWebmailMessageRequest.php @@ -0,0 +1,40 @@ + + */ + public function rules(): array + { + return [ + 'from_email' => ['required', 'email:rfc,dns'], + 'from_name' => ['nullable', 'string', 'max:255'], + 'to' => ['required', 'array', 'min:1'], + 'to.*' => ['required', 'email:rfc,dns'], + 'cc' => ['nullable', 'array'], + 'cc.*' => ['email:rfc,dns'], + 'bcc' => ['nullable', 'array'], + 'bcc.*' => ['email:rfc,dns'], + 'subject' => ['nullable', 'string', 'max:255'], + 'body' => ['nullable', 'string'], + 'folder' => ['nullable', 'string', 'max:30'], + 'status' => ['nullable', 'string', 'max:30'], + 'received_at' => ['nullable', 'date'], + 'attachments' => ['nullable', 'array'], + 'metadata' => ['nullable', 'array'], + 'message_uid' => ['nullable', 'string', 'max:255'], + ]; + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Http/Requests/SendWebmailMessageRequest.php b/thanasoft-back/app/Http/Requests/SendWebmailMessageRequest.php new file mode 100644 index 0000000..c5b56a2 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/SendWebmailMessageRequest.php @@ -0,0 +1,36 @@ + + */ + public function rules(): array + { + return [ + 'to' => ['required', 'array', 'min:1'], + 'to.*' => ['required', 'email:rfc,dns'], + 'cc' => ['nullable', 'array'], + 'cc.*' => ['email:rfc,dns'], + 'bcc' => ['nullable', 'array'], + 'bcc.*' => ['email:rfc,dns'], + 'subject' => ['nullable', 'string', 'max:255'], + 'body' => ['required', 'string'], + 'folder' => ['nullable', 'string', 'max:30'], + 'attachments' => ['nullable', 'array'], + 'metadata' => ['nullable', 'array'], + 'message_uid' => ['nullable', 'string', 'max:255'], + ]; + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Http/Requests/StoreAvoirRequest.php b/thanasoft-back/app/Http/Requests/StoreAvoirRequest.php new file mode 100644 index 0000000..2e5202a --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreAvoirRequest.php @@ -0,0 +1,48 @@ + 'required|exists:clients,id', + 'invoice_id' => 'nullable|exists:invoices,id', + 'group_id' => 'nullable|exists:client_groups,id', + 'status' => 'required|in:brouillon,emis,applique,annule', + 'avoir_date' => 'required|date', + 'due_date' => 'nullable|date|after_or_equal:avoir_date', + 'currency' => 'required|string|size:3', + 'total_ht' => 'required|numeric', + 'total_tva' => 'required|numeric', + 'total_ttc' => 'required|numeric', + 'reason_type' => 'required|in:remboursement_total,remboursement_partiel,reduction,erreur_facturation,retour_marchandise,accord_commercial,autre', + 'reason_description' => 'nullable|string', + 'e_invoice_status' => 'nullable|in:cree,transmis,accepte,refuse,en_litige,acquitte,archive', + 'refund_status' => 'nullable|in:non_rembourse,en_cours,partiellement_rembourse,rembourse,compense', + 'refund_date' => 'nullable|date', + 'refund_method' => 'nullable|in:virement,cheque,carte_credit,compensation_future,autre', + 'compensation_invoice_id' => 'nullable|exists:invoices,id', + 'compensation_amount' => 'nullable|numeric|min:0', + 'lines' => 'required|array|min:1', + 'lines.*.product_id' => 'nullable|exists:products,id', + 'lines.*.invoice_line_id' => 'nullable|exists:invoice_lines,id', + 'lines.*.description' => 'required|string', + 'lines.*.quantity' => 'required|numeric', + 'lines.*.unit_price' => 'required|numeric', + 'lines.*.tva_rate' => 'required|numeric', + 'lines.*.total_ht' => 'required|numeric', + 'lines.*.total_tva' => 'required|numeric', + 'lines.*.total_ttc' => 'required|numeric', + 'lines.*.notes' => 'nullable|string', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreClientGroupRequest.php b/thanasoft-back/app/Http/Requests/StoreClientGroupRequest.php new file mode 100644 index 0000000..18156ec --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreClientGroupRequest.php @@ -0,0 +1,46 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'required|string|max:191|unique:client_groups,name', + 'description' => 'nullable|string', + 'client_ids' => 'sometimes|array', + 'client_ids.*' => 'integer|distinct|exists:clients,id', + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Le nom du groupe est obligatoire.', + 'name.string' => 'Le nom du groupe doit être une chaîne de caractères.', + 'name.max' => 'Le nom du groupe ne peut pas dépasser 191 caractères.', + 'name.unique' => 'Un groupe avec ce nom existe déjà.', + 'description.string' => 'La description doit être une chaîne de caractères.', + 'client_ids.array' => 'La liste des clients doit être un tableau.', + 'client_ids.*.integer' => 'Chaque ID client doit être un entier.', + 'client_ids.*.distinct' => 'Un client ne peut pas être sélectionné plusieurs fois.', + 'client_ids.*.exists' => 'Un ou plusieurs clients sélectionnés sont introuvables.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreClientLocationRequest.php b/thanasoft-back/app/Http/Requests/StoreClientLocationRequest.php new file mode 100644 index 0000000..88cff52 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreClientLocationRequest.php @@ -0,0 +1,68 @@ +|string> + */ + public function rules(): array + { + return [ + 'client_id' => 'required|exists:clients,id', + 'name' => 'nullable|string|max:191', + 'address_line1' => 'nullable|string|max:255', + 'address_line2' => 'nullable|string|max:255', + 'postal_code' => 'nullable|string|max:20', + 'city' => 'nullable|string|max:191', + 'country_code' => 'nullable|string|size:2', + 'gps_lat' => 'nullable|numeric|between:-90,90', + 'gps_lng' => 'nullable|numeric|between:-180,180', + 'is_default' => 'boolean', + ]; + } + + public function messages(): array + { + return [ + 'client_id.required' => 'Le client est obligatoire.', + 'client_id.exists' => 'Le client sélectionné n\'existe pas.', + 'name.max' => 'Le nom ne peut pas dépasser 191 caractères.', + 'address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.', + 'address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.', + 'postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.', + 'city.max' => 'La ville ne peut pas dépasser 191 caractères.', + 'country_code.size' => 'Le code pays doit contenir 2 caractères.', + 'gps_lat.numeric' => 'La latitude doit être un nombre.', + 'gps_lat.between' => 'La latitude doit être comprise entre -90 et 90.', + 'gps_lng.numeric' => 'La longitude doit être un nombre.', + 'gps_lng.between' => 'La longitude doit être comprise entre -180 et 180.', + 'is_default.boolean' => 'Le statut par défaut doit être vrai ou faux.', + ]; + } + + public function withValidator($validator) + { + $validator->after(function ($validator) { + if (empty($this->address_line1) && empty($this->postal_code) && empty($this->city)) { + $validator->errors()->add( + 'general', + 'Au moins un champ d\'adresse (adresse, code postal ou ville) doit être renseigné.' + ); + } + }); + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreClientRequest.php b/thanasoft-back/app/Http/Requests/StoreClientRequest.php new file mode 100644 index 0000000..31ec8f4 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreClientRequest.php @@ -0,0 +1,72 @@ +|string> + */ + public function rules(): array + { + return [ + 'client_category_id' => 'nullable', + 'name' => 'required|string|max:255', + 'vat_number' => 'nullable|string|max:32', + 'siret' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:191', + 'phone' => 'nullable|string|max:50', + 'billing_address_line1' => 'required|string|max:255', + 'billing_address_line2' => 'nullable|string|max:255', + 'billing_postal_code' => 'nullable|string|max:20', + 'billing_city' => 'nullable|string|max:191', + 'billing_country_code' => 'nullable|string|size:2', + 'group_id' => 'nullable|exists:client_groups,id', + 'notes' => 'nullable|string', + 'is_active' => 'boolean', + 'is_parent' => 'boolean|nullable', + 'parent_id' => 'nullable|exists:clients,id', + 'default_tva_rate_id' => 'nullable|exists:tva_rates,id', + ]; + } + + + public function messages(): array + { + return [ + 'company_id.required' => 'La société est obligatoire.', + 'company_id.exists' => 'La société sélectionnée n\'existe pas.', + 'type.required' => 'Le type de client est obligatoire.', + 'type.in' => 'Le type de client sélectionné est invalide.', + 'name.required' => 'Le nom du client est obligatoire.', + 'name.string' => 'Le nom du client doit être une chaîne de caractères.', + 'name.max' => 'Le nom du client ne peut pas dépasser 255 caractères.', + 'vat_number.max' => 'Le numéro de TVA ne peut pas dépasser 32 caractères.', + 'siret.max' => 'Le SIRET ne peut pas dépasser 20 caractères.', + 'email.email' => 'L\'adresse email doit être valide.', + 'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.', + 'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.', + 'billing_address_line1.required' => 'L\'adresse facturation est obligatoire.', + 'billing_address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.', + 'billing_address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.', + 'billing_postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.', + 'billing_city.max' => 'La ville ne peut pas dépasser 191 caractères.', + 'billing_country_code.size' => 'Le code pays doit contenir 2 caractères.', + 'group_id.exists' => 'Le groupe de clients sélectionné n\'existe pas.', + 'is_active.boolean' => 'Le statut actif doit être vrai ou faux.', + 'default_tva_rate_id.exists' => 'Le taux de TVA sélectionné n\'existe pas.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreContactRequest.php b/thanasoft-back/app/Http/Requests/StoreContactRequest.php new file mode 100644 index 0000000..26f719c --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreContactRequest.php @@ -0,0 +1,70 @@ +|string> + */ + public function rules(): array + { + return [ + 'client_id' => 'nullable|exists:clients,id', + 'fournisseur_id' => 'nullable|exists:fournisseurs,id', + 'first_name' => 'nullable|string|max:191', + 'last_name' => 'nullable|string|max:191', + 'email' => 'nullable|email|max:191', + 'phone' => 'nullable|string|max:50', + 'role' => 'nullable|string|max:191', + ]; + } + + public function messages(): array + { + return [ + 'client_id.exists' => 'Le client sélectionné n\'existe pas.', + 'fournisseur_id.exists' => 'Le fournisseur sélectionné n\'existe pas.', + 'first_name.string' => 'Le prénom doit être une chaîne de caractères.', + 'first_name.max' => 'Le prénom ne peut pas dépasser 191 caractères.', + 'last_name.string' => 'Le nom doit être une chaîne de caractères.', + 'last_name.max' => 'Le nom ne peut pas dépasser 191 caractères.', + 'email.email' => 'L\'adresse email doit être valide.', + 'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.', + 'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.', + 'role.max' => 'Le rôle ne peut pas dépasser 191 caractères.', + ]; + } + + public function withValidator($validator) + { + $validator->after(function ($validator) { + // At least one of client_id or fournisseur_id must be provided + if (empty($this->client_id) && empty($this->fournisseur_id)) { + $validator->errors()->add( + 'general', + 'Le contact doit être associé à un client ou un fournisseur.' + ); + } + + if (empty($this->first_name) && empty($this->last_name) && empty($this->email) && empty($this->phone)) { + $validator->errors()->add( + 'general', + 'Au moins un champ (prénom, nom, email ou téléphone) doit être renseigné.' + ); + } + }); + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreConvoyRequest.php b/thanasoft-back/app/Http/Requests/StoreConvoyRequest.php new file mode 100644 index 0000000..6faa002 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreConvoyRequest.php @@ -0,0 +1,41 @@ + ['required', 'exists:deceased,id'], + 'client_id' => ['nullable', 'exists:clients,id'], + 'vehicle_id' => ['nullable', 'exists:vehicles,id'], + 'mission_title' => ['nullable', 'string', 'max:255'], + 'convoy_type' => ['nullable', Rule::in(['local', 'national', 'international'])], + 'transport_mode' => ['nullable', Rule::in(['road', 'air', 'sea', 'rail'])], + 'status' => ['nullable', Rule::in(['planned', 'in_progress', 'completed', 'cancelled'])], + 'planned_start_at' => ['required', 'date'], + 'estimated_end_at' => ['nullable', 'date', 'after_or_equal:planned_start_at'], + 'family_email' => ['nullable', 'email', 'max:255'], + 'automatic_notifications' => ['nullable', 'boolean'], + 'departure_location_selection_mode' => ['nullable', Rule::in(['place', 'manual'])], + 'departure_location_id' => ['nullable', 'exists:client_locations,id'], + 'departure_name' => ['nullable', 'string', 'max:255'], + 'departure_address' => ['nullable', 'string', 'max:255'], + 'departure_city' => ['nullable', 'string', 'max:255'], + 'departure_postal_code' => ['nullable', 'string', 'max:20'], + 'departure_country_code' => ['nullable', 'string', 'size:2'], + 'departure_latitude' => ['nullable', 'numeric', 'between:-90,90'], + 'departure_longitude' => ['nullable', 'numeric', 'between:-180,180'], + 'departure_additional_details' => ['nullable', 'string'], + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreDeceasedDocumentRequest.php b/thanasoft-back/app/Http/Requests/StoreDeceasedDocumentRequest.php new file mode 100644 index 0000000..9957c3b --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreDeceasedDocumentRequest.php @@ -0,0 +1,49 @@ + + */ + public function rules(): array + { + return [ + 'deceased_id' => 'required|exists:deceased,id', + 'doc_type' => 'required|string|max:191', + 'file_id' => 'nullable|exists:files,id', + 'generated_at' => 'nullable|date', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'deceased_id.required' => 'Le défunt est obligatoire.', + 'deceased_id.exists' => 'Le défunt sélectionné n\'existe pas.', + 'doc_type.required' => 'Le type de document est obligatoire.', + 'doc_type.string' => 'Le type de document doit être une chaîne de caractères.', + 'doc_type.max' => 'Le type de document ne peut pas dépasser :max caractères.', + 'file_id.exists' => 'Le fichier sélectionné n\'existe pas.', + 'generated_at.date' => 'La date de génération doit être une date valide.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreDeceasedRequest.php b/thanasoft-back/app/Http/Requests/StoreDeceasedRequest.php new file mode 100644 index 0000000..1f75f24 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreDeceasedRequest.php @@ -0,0 +1,49 @@ +|string> + */ + public function rules(): array + { + return [ + 'last_name' => ['required', 'string', 'max:191'], + 'first_name' => ['nullable', 'string', 'max:191'], + 'birth_date' => ['nullable', 'date'], + 'death_date' => ['nullable', 'date', 'after_or_equal:birth_date'], + 'place_of_death' => ['nullable', 'string', 'max:255'], + 'notes' => ['nullable', 'string'] + ]; + } + + /** + * Get custom error messages for validator errors. + */ + public function messages(): array + { + return [ + 'last_name.required' => 'Le nom de famille est obligatoire.', + 'last_name.max' => 'Le nom de famille ne peut pas dépasser 191 caractères.', + 'first_name.max' => 'Le prénom ne peut pas dépasser 191 caractères.', + 'death_date.after_or_equal' => 'La date de décès doit être postérieure ou égale à la date de naissance.', + 'place_of_death.max' => 'Le lieu de décès ne peut pas dépasser 255 caractères.' + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreEmployeeRequest.php b/thanasoft-back/app/Http/Requests/StoreEmployeeRequest.php new file mode 100644 index 0000000..93fc0b1 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreEmployeeRequest.php @@ -0,0 +1,59 @@ + + */ + public function rules(): array + { + return [ + 'first_name' => 'required|string|max:191', + 'last_name' => 'required|string|max:191', + 'email' => 'nullable|email|max:191|unique:employees,email', + 'phone' => 'nullable|string|max:50', + 'job_title' => 'nullable|string|max:191', + 'hire_date' => 'nullable|date', + 'active' => 'boolean', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'first_name.required' => 'Le prénom est obligatoire.', + 'first_name.string' => 'Le prénom doit être une chaîne de caractères.', + 'first_name.max' => 'Le prénom ne peut pas dépasser :max caractères.', + 'last_name.required' => 'Le nom de famille est obligatoire.', + 'last_name.string' => 'Le nom de famille doit être une chaîne de caractères.', + 'last_name.max' => 'Le nom de famille ne peut pas dépasser :max caractères.', + 'email.email' => 'L\'adresse email doit être valide.', + 'email.unique' => 'Cette adresse email est déjà utilisée.', + 'phone.string' => 'Le téléphone doit être une chaîne de caractères.', + 'phone.max' => 'Le téléphone ne peut pas dépasser :max caractères.', + 'job_title.string' => 'L\'intitulé du poste doit être une chaîne de caractères.', + 'job_title.max' => 'L\'intitulé du poste ne peut pas dépasser :max caractères.', + 'hire_date.date' => 'La date d\'embauche doit être une date valide.', + 'active.boolean' => 'Le statut actif doit être un booléen.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreFileRequest.php b/thanasoft-back/app/Http/Requests/StoreFileRequest.php new file mode 100644 index 0000000..fee7133 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreFileRequest.php @@ -0,0 +1,109 @@ +|string> + */ + public function rules(): array + { + return [ + 'file' => 'required|file|max:10240', // Max 10MB + 'file_name' => 'nullable|string|max:255', + 'category' => 'required|string|in:devis,facture,contrat,document,image,autre', + 'client_id' => 'nullable|integer|exists:clients,id', + 'subcategory' => 'nullable|string|max:100', + 'description' => 'nullable|string|max:500', + 'tags' => 'nullable|array|max:10', + 'tags.*' => 'string|max:50', + 'is_public' => 'boolean', + ]; + } + + /** + * Get custom attributes for validator errors. + */ + public function attributes(): array + { + return [ + 'file' => 'fichier', + 'file_name' => 'nom du fichier', + 'category' => 'catégorie', + 'client_id' => 'client', + 'subcategory' => 'sous-catégorie', + 'description' => 'description', + 'tags' => 'étiquettes', + 'is_public' => 'visibilité publique', + ]; + } + + /** + * Get the error messages for the defined validation rules. + */ + public function messages(): array + { + return [ + 'file.required' => 'Le fichier est obligatoire.', + 'file.file' => 'Le fichier doit être un fichier valide.', + 'file.max' => 'Le fichier ne peut pas dépasser 10 MB.', + 'file_name.string' => 'Le nom du fichier doit être une chaîne de caractères.', + 'file_name.max' => 'Le nom du fichier ne peut pas dépasser 255 caractères.', + 'category.required' => 'La catégorie est obligatoire.', + 'category.in' => 'La catégorie sélectionnée n\'est pas valide.', + 'client_id.exists' => 'Le client sélectionné n\'existe pas.', + 'subcategory.string' => 'La sous-catégorie doit être une chaîne de caractères.', + 'subcategory.max' => 'La sous-catégorie ne peut pas dépasser 100 caractères.', + 'description.string' => 'La description doit être une chaîne de caractères.', + 'description.max' => 'La description ne peut pas dépasser 500 caractères.', + 'tags.array' => 'Les étiquettes doivent être un tableau.', + 'tags.max' => 'Vous ne pouvez pas ajouter plus de 10 étiquettes.', + 'tags.*.string' => 'Chaque étiquette doit être une chaîne de caractères.', + 'tags.*.max' => 'Chaque étiquette ne peut pas dépasser 50 caractères.', + 'is_public.boolean' => 'La visibilité publique doit être vrai ou faux.', + ]; + } + + /** + * Prepare the data for validation. + */ + protected function prepareForValidation(): void + { + // Set default values + $this->merge([ + 'uploaded_by' => $this->user()->id, + 'is_public' => $this->boolean('is_public', false), + 'category' => $this->input('category', 'autre'), // Default category to 'autre' if not provided + ]); + + // If no file_name provided, use the original file name + if (!$this->has('file_name') && $this->hasFile('file')) { + $this->merge([ + 'file_name' => $this->file->getClientOriginalName(), + ]); + } + + // Extract file information + if ($this->hasFile('file')) { + $file = $this->file; + $this->merge([ + 'mime_type' => $file->getMimeType(), + 'size_bytes' => $file->getSize(), + ]); + } + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreFournisseurRequest.php b/thanasoft-back/app/Http/Requests/StoreFournisseurRequest.php new file mode 100644 index 0000000..2e6364b --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreFournisseurRequest.php @@ -0,0 +1,59 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'required|string|max:255', + 'vat_number' => 'nullable|string|max:32', + 'siret' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:191', + 'phone' => 'nullable|string|max:50', + 'billing_address_line1' => 'nullable|string|max:255', + 'billing_address_line2' => 'nullable|string|max:255', + 'billing_postal_code' => 'nullable|string|max:20', + 'billing_city' => 'nullable|string|max:191', + 'billing_country_code' => 'nullable|string|size:2', + 'notes' => 'nullable|string', + 'is_active' => 'boolean', + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Le nom du fournisseur est obligatoire.', + 'name.string' => 'Le nom du fournisseur doit être une chaîne de caractères.', + 'name.max' => 'Le nom du fournisseur ne peut pas dépasser 255 caractères.', + 'vat_number.max' => 'Le numéro de TVA ne peut pas dépasser 32 caractères.', + 'siret.max' => 'Le SIRET ne peut pas dépasser 20 caractères.', + 'email.email' => 'L\'adresse email doit être valide.', + 'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.', + 'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.', + 'billing_address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.', + 'billing_address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.', + 'billing_postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.', + 'billing_city.max' => 'La ville ne peut pas dépasser 191 caractères.', + 'billing_country_code.size' => 'Le code pays doit contenir 2 caractères.', + 'is_active.boolean' => 'Le statut actif doit être vrai ou faux.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreGoodsReceiptRequest.php b/thanasoft-back/app/Http/Requests/StoreGoodsReceiptRequest.php new file mode 100644 index 0000000..2973073 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreGoodsReceiptRequest.php @@ -0,0 +1,79 @@ +|string> + */ + public function rules(): array + { + return [ + 'purchase_order_id' => 'required|exists:purchase_orders,id', + 'warehouse_id' => 'required|exists:warehouses,id', + 'receipt_number' => 'required|string|max:191', + 'receipt_date' => 'required|date', + 'status' => 'nullable|in:draft,posted', + 'notes' => 'nullable|string', + 'lines' => 'nullable|array', + 'lines.*.product_id' => 'required_with:lines|exists:products,id', + 'lines.*.packaging_id' => 'nullable|exists:product_packagings,id', + 'lines.*.packages_qty_received' => 'nullable|numeric|min:0', + 'lines.*.units_qty_received' => 'nullable|numeric|min:0', + 'lines.*.qty_received_base' => 'nullable|numeric|min:0', + 'lines.*.unit_price' => 'nullable|numeric|min:0', + 'lines.*.unit_price_per_package' => 'nullable|numeric|min:0', + 'lines.*.tva_rate_id' => 'nullable|exists:tva_rates,id', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'purchase_order_id.required' => 'La commande fournisseur est requise.', + 'purchase_order_id.exists' => 'La commande fournisseur spécifiée n\'existe pas.', + 'warehouse_id.required' => 'L\'entrepôt est requis.', + 'warehouse_id.exists' => 'L\'entrepôt spécifié n\'existe pas.', + 'receipt_number.required' => 'Le numéro de réception est requis.', + 'receipt_number.string' => 'Le numéro de réception doit être une chaîne de caractères.', + 'receipt_number.max' => 'Le numéro de réception ne peut pas dépasser 191 caractères.', + 'receipt_date.required' => 'La date de réception est requise.', + 'receipt_date.date' => 'La date de réception doit être une date valide.', + 'status.in' => 'Le statut doit être "draft" ou "posted".', + 'notes.string' => 'Les notes doivent être une chaîne de caractères.', + 'lines.array' => 'Les lignes doivent être un tableau.', + 'lines.*.product_id.required_with' => 'Le produit est requis pour chaque ligne.', + 'lines.*.product_id.exists' => 'Le produit spécifié dans une ligne n\'existe pas.', + 'lines.*.packaging_id.exists' => 'Le conditionnement spécifié dans une ligne n\'existe pas.', + 'lines.*.packages_qty_received.numeric' => 'La quantité de colis doit être un nombre.', + 'lines.*.packages_qty_received.min' => 'La quantité de colis ne peut pas être négative.', + 'lines.*.units_qty_received.numeric' => 'La quantité d\'unités doit être un nombre.', + 'lines.*.units_qty_received.min' => 'La quantité d\'unités ne peut pas être négative.', + 'lines.*.qty_received_base.numeric' => 'La quantité de base doit être un nombre.', + 'lines.*.qty_received_base.min' => 'La quantité de base ne peut pas être négative.', + 'lines.*.unit_price.numeric' => 'Le prix unitaire doit être un nombre.', + 'lines.*.unit_price.min' => 'Le prix unitaire ne peut pas être négatif.', + 'lines.*.unit_price_per_package.numeric' => 'Le prix par colis doit être un nombre.', + 'lines.*.unit_price_per_package.min' => 'Le prix par colis ne peut pas être négatif.', + 'lines.*.tva_rate_id.exists' => 'Le taux de TVA spécifié dans une ligne n\'existe pas.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreInterventionRequest.php b/thanasoft-back/app/Http/Requests/StoreInterventionRequest.php new file mode 100644 index 0000000..4d27545 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreInterventionRequest.php @@ -0,0 +1,84 @@ +|string> + */ + public function rules(): array + { + return [ + 'client_id' => ['required', 'exists:clients,id'], + 'deceased_id' => ['nullable', 'exists:deceased,id'], + 'order_giver' => ['nullable', 'string', 'max:255'], + 'location_id' => ['nullable', 'exists:client_locations,id'], + 'product_id' => ['nullable', 'exists:products,id'], + 'type' => ['required', Rule::in([ + 'thanatopraxie', + 'toilette_mortuaire', + 'exhumation', + 'retrait_pacemaker', + 'retrait_bijoux', + 'autre' + ])], + 'scheduled_at' => ['nullable', 'date_format:Y-m-d H:i:s'], + 'duration_min' => ['nullable', 'integer', 'min:0'], + 'status' => ['sometimes', Rule::in([ + 'demande', + 'planifie', + 'en_cours', + 'termine', + 'annule' + ])], + 'practitioners' => ['nullable', 'array'], + 'practitioners.*' => ['exists:thanatopractitioners,id'], + 'principal_practitioner_id' => ['nullable', 'exists:thanatopractitioners,id'], + 'assistant_practitioner_ids' => ['nullable', 'array'], + 'assistant_practitioner_ids.*' => ['exists:thanatopractitioners,id'], + 'notes' => ['nullable', 'string'], + 'created_by' => ['nullable', 'exists:users,id'] + ]; + } + + /** + * Get custom error messages for validator errors. + */ + public function messages(): array + { + return [ + 'client_id.required' => 'Le client est obligatoire.', + 'client_id.exists' => 'Le client sélectionné est invalide.', + 'deceased_id.exists' => 'Le défunt sélectionné est invalide.', + 'order_giver.max' => 'Le donneur d\'ordre ne peut pas dépasser 255 caractères.', + 'location_id.exists' => 'Le lieu sélectionné est invalide.', + 'type.required' => 'Le type d\'intervention est obligatoire.', + 'type.in' => 'Le type d\'intervention est invalide.', + 'scheduled_at.date_format' => 'Le format de la date programmée est invalide.', + 'duration_min.integer' => 'La durée doit être un nombre entier.', + 'duration_min.min' => 'La durée ne peut pas être négative.', + 'status.in' => 'Le statut de l\'intervention est invalide.', + 'practitioners.array' => 'Les praticiens doivent être un tableau.', + 'practitioners.*.exists' => 'Un des praticiens sélectionnés est invalide.', + 'principal_practitioner_id.exists' => 'Le praticien principal sélectionné est invalide.', + 'assistant_practitioner_ids.array' => 'Les praticiens assistants doivent être un tableau.', + 'assistant_practitioner_ids.*.exists' => 'Un des praticiens assistants est invalide.', + 'created_by.exists' => 'L\'utilisateur créateur est invalide.' + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreInterventionWithAllDataRequest.php b/thanasoft-back/app/Http/Requests/StoreInterventionWithAllDataRequest.php new file mode 100644 index 0000000..44628bf --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreInterventionWithAllDataRequest.php @@ -0,0 +1,205 @@ +|string> + */ + public function rules(): array + { + return [ + 'deceased_id' => ['nullable', 'exists:deceased,id'], + 'deceased' => ['required_without:deceased_id', 'array'], + 'deceased.last_name' => ['required_without:deceased_id', 'string', 'max:191'], + 'deceased.first_name' => ['nullable', 'string', 'max:191'], + 'deceased.birth_date' => ['nullable', 'date'], + 'deceased.death_date' => ['nullable', 'date', 'after_or_equal:deceased.birth_date'], + 'deceased.place_of_death' => ['nullable', 'string', 'max:255'], + 'deceased.notes' => ['nullable', 'string'], + + 'client_id' => ['nullable', 'exists:clients,id'], + 'client' => 'required_without:client_id|array', + 'client.name' => ['required', 'string', 'max:255'], + 'client.vat_number' => ['nullable', 'string', 'max:32'], + 'client.siret' => ['nullable', 'string', 'max:20'], + 'client.email' => ['nullable', 'email', 'max:191'], + 'client.phone' => ['nullable', 'string', 'max:50'], + 'client.billing_address_line1' => ['nullable', 'string', 'max:255'], + 'client.billing_address_line2' => ['nullable', 'string', 'max:255'], + 'client.billing_postal_code' => ['nullable', 'string', 'max:20'], + 'client.billing_city' => ['nullable', 'string', 'max:191'], + 'client.billing_country_code' => ['nullable', 'string', 'size:2'], + 'client.notes' => ['nullable', 'string'], + + 'contact' => 'nullable|array', + 'contact.first_name' => ['nullable', 'string', 'max:191'], + 'contact.last_name' => ['nullable', 'string', 'max:191'], + 'contact.email' => ['nullable', 'email', 'max:191'], + 'contact.phone' => ['nullable', 'string', 'max:50'], + 'contact.role' => ['nullable', 'string', 'max:191'], + + 'location_id' => ['nullable', 'exists:client_locations,id'], + 'location' => ['nullable', 'array'], + 'location.name' => ['required_without:location_id', 'string', 'max:255'], + 'location.address' => ['nullable', 'string', 'max:255'], + 'location.city' => ['required_without:location_id', 'string', 'max:191'], + 'location.postal_code' => ['nullable', 'string', 'max:20'], + 'location.country_code' => ['nullable', 'string', 'size:2'], + 'location.access_instructions' => ['nullable', 'string'], + 'location.notes' => ['nullable', 'string'], + + 'documents' => 'nullable|array', + 'documents.*.file' => ['required', 'file'], + 'documents.*.name' => ['required', 'string', 'max:255'], + 'documents.*.description' => ['nullable', 'string'], + + 'intervention' => 'required|array', + 'intervention.type' => [ + 'required', + Rule::in([ + 'thanatopraxie', + 'toilette_mortuaire', + 'exhumation', + 'retrait_pacemaker', + 'retrait_bijoux', + 'autre' + ]) + ], + 'intervention.scheduled_at' => ['nullable', 'date_format:Y-m-d H:i:s'], + 'intervention.duration_min' => ['nullable', 'integer', 'min:0'], + 'intervention.status' => [ + 'sometimes', + Rule::in([ + 'demande', + 'planifie', + 'en_cours', + 'termine', + 'annule' + ]) + ], + 'intervention.practitioners' => ['nullable', 'array'], + 'intervention.practitioners.*' => ['exists:thanatopractitioners,id'], + 'intervention.principal_practitioner_id' => ['nullable', 'exists:thanatopractitioners,id'], + 'intervention.assistant_practitioner_ids' => ['nullable', 'array'], + 'intervention.assistant_practitioner_ids.*' => ['exists:thanatopractitioners,id'], + 'intervention.order_giver' => ['nullable', 'string', 'max:255'], + 'intervention.notes' => ['nullable', 'string'], + 'intervention.created_by' => ['nullable', 'exists:users,id'] + ]; + } + + /** + * Get custom error messages for validator errors. + */ + public function messages(): array + { + $messages = [ + 'deceased.required' => 'Les informations du défunt sont obligatoires.', + 'deceased.last_name.required' => 'Le nom de famille du défunt est obligatoire.', + 'deceased.last_name.max' => 'Le nom de famille ne peut pas dépasser 191 caractères.', + 'deceased.first_name.max' => 'Le prénom ne peut pas dépasser 191 caractères.', + 'deceased.death_date.after_or_equal' => 'La date de décès doit être postérieure ou égale à la date de naissance.', + 'deceased.place_of_death.max' => 'Le lieu de décès ne peut pas dépasser 255 caractères.', + + 'client.required' => 'Les informations du client sont obligatoires.', + 'client.name.required' => 'Le nom du client est obligatoire.', + 'client.name.max' => 'Le nom du client ne peut pas dépasser 255 caractères.', + 'client.vat_number.max' => 'Le numéro de TVA ne peut pas dépasser 32 caractères.', + 'client.siret.max' => 'Le SIRET ne peut pas dépasser 20 caractères.', + 'client.email.email' => 'L\'adresse email doit être valide.', + 'client.email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.', + 'client.phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.', + 'client.billing_address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.', + 'client.billing_postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.', + 'client.billing_city.max' => 'La ville ne peut pas dépasser 191 caractères.', + 'client.billing_country_code.size' => 'Le code pays doit contenir 2 caractères.', + 'client.is_active.boolean' => 'Le statut actif doit être vrai ou faux.', + + 'contact.first_name.max' => 'Le prénom ne peut pas dépasser 191 caractères.', + 'contact.last_name.max' => 'Le nom ne peut pas dépasser 191 caractères.', + 'contact.email.email' => 'L\'adresse email doit être valide.', + 'contact.email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.', + 'contact.phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.', + 'contact.role.max' => 'Le rôle ne peut pas dépasser 191 caractères.', + + 'intervention.required' => 'Les informations de l\'intervention sont obligatoires.', + 'intervention.type.required' => 'Le type d\'intervention est obligatoire.', + 'intervention.type.in' => 'Le type d\'intervention est invalide.', + 'intervention.scheduled_at.date_format' => 'Le format de la date programmée est invalide.', + 'intervention.duration_min.integer' => 'La durée doit être un nombre entier.', + 'intervention.duration_min.min' => 'La durée ne peut pas être négative.', + 'intervention.status.in' => 'Le statut de l\'intervention est invalide.', + 'intervention.practitioners.array' => 'Les praticiens doivent être un tableau.', + 'intervention.practitioners.*.exists' => 'Un des praticiens sélectionnés est invalide.', + 'intervention.principal_practitioner_id.exists' => 'Le praticien principal sélectionné est invalide.', + 'intervention.assistant_practitioner_ids.array' => 'Les praticiens assistants doivent être un tableau.', + 'intervention.assistant_practitioner_ids.*.exists' => 'Un des praticiens assistants est invalide.', + 'intervention.order_giver.max' => 'Le donneur d\'ordre ne peut pas dépasser 255 caractères.', + 'intervention.created_by.exists' => 'L\'utilisateur créateur est invalide.' + ]; + + // Add document-specific messages + $messages = array_merge($messages, [ + 'documents.array' => 'Les documents doivent être un tableau.', + 'documents.*.file.required' => 'Chaque document doit avoir un fichier.', + 'documents.*.name.required' => 'Le nom du document est obligatoire.', + 'documents.*.name.max' => 'Le nom du document ne peut pas dépasser 255 caractères.', + ]); + + return $messages; + } + + /** + * Handle a failed validation attempt. + */ + protected function failedValidation(\Illuminate\Contracts\Validation\Validator $validator) + { + $errors = $validator->errors(); + + // Group errors by step for better UX + $groupedErrors = [ + 'deceased' => [], + 'client' => [], + 'contact' => [], + 'location' => [], + 'documents' => [], + 'intervention' => [], + 'global' => [] + ]; + + foreach ($errors->messages() as $field => $messages) { + if (str_starts_with($field, 'deceased.')) { + $groupedErrors['deceased'] = array_merge($groupedErrors['deceased'], $messages); + } elseif (str_starts_with($field, 'client.')) { + $groupedErrors['client'] = array_merge($groupedErrors['client'], $messages); + } elseif (str_starts_with($field, 'contact.')) { + $groupedErrors['contact'] = array_merge($groupedErrors['contact'], $messages); + } elseif (str_starts_with($field, 'location.')) { + $groupedErrors['location'] = array_merge($groupedErrors['location'], $messages); + } elseif (str_starts_with($field, 'documents.')) { + $groupedErrors['documents'] = array_merge($groupedErrors['documents'], $messages); + } elseif (str_starts_with($field, 'intervention.')) { + $groupedErrors['intervention'] = array_merge($groupedErrors['intervention'], $messages); + } else { + $groupedErrors['global'] = array_merge($groupedErrors['global'], $messages); + } + } + + parent::failedValidation($validator); + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreInvoiceRequest.php b/thanasoft-back/app/Http/Requests/StoreInvoiceRequest.php new file mode 100644 index 0000000..2f5168d --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreInvoiceRequest.php @@ -0,0 +1,99 @@ +|string> + */ + public function rules(): array + { + return [ + 'client_id' => 'nullable|exists:clients,id', + 'group_id' => 'nullable|exists:client_groups,id', + 'source_quote_id' => 'nullable|exists:quotes,id', + 'status' => 'required|in:brouillon,emise,envoyee,partiellement_payee,payee,echue,annulee,avoir', + 'invoice_date' => 'required|date', + 'due_date' => 'nullable|date|after_or_equal:invoice_date', + 'currency' => 'required|string|size:3', + 'total_ht' => 'required|numeric|min:0', + 'total_tva' => 'required|numeric|min:0', + 'total_ttc' => 'required|numeric|min:0', + 'e_invoicing_channel_id' => 'nullable|exists:e_invoicing_channels,id', + 'e_invoice_status' => 'nullable|in:cree,transmis,accepte,refuse,en_litige,acquitte,archive', + 'lines' => 'required|array|min:1', + 'lines.*.product_id' => 'nullable|exists:products,id', + 'lines.*.packaging_id' => 'nullable|exists:product_packagings,id', + 'lines.*.packages_qty' => 'nullable|numeric|min:0', + 'lines.*.units_qty' => 'nullable|numeric|min:0', + 'lines.*.description' => 'required|string', + 'lines.*.qty_base' => 'nullable|numeric|min:0', + 'lines.*.unit_price' => 'required|numeric|min:0', + 'lines.*.unit_price_per_package' => 'nullable|numeric|min:0', + 'lines.*.tva_rate_id' => 'nullable|exists:tva_rates,id', + 'lines.*.discount_pct' => 'required|numeric|min:0|max:100', + 'lines.*.total_ht' => 'required|numeric|min:0', + ]; + } + + public function withValidator($validator): void + { + $validator->after(function ($validator) { + $hasClient = filled($this->input('client_id')); + $hasGroup = filled($this->input('group_id')); + + if (! $hasClient && ! $hasGroup) { + $message = 'Un client ou un groupe de clients est obligatoire.'; + + $validator->errors()->add('client_id', $message); + $validator->errors()->add('group_id', $message); + } + + if ($hasClient && $hasGroup) { + $message = 'Selectionnez soit un client, soit un groupe de clients.'; + + $validator->errors()->add('client_id', $message); + $validator->errors()->add('group_id', $message); + } + }); + } + + public function messages(): array + { + return [ + 'client_id.exists' => 'Le client sélectionné est invalide.', + 'group_id.exists' => 'Le groupe sélectionné est invalide.', + 'status.required' => 'Le statut est obligatoire.', + 'status.in' => 'Le statut sélectionné est invalide.', + 'invoice_date.required' => 'La date de la facture est obligatoire.', + 'invoice_date.date' => 'La date de la facture n\'est pas valide.', + 'due_date.date' => 'La date d\'échéance n\'est pas valide.', + 'due_date.after_or_equal' => 'La date d\'échéance doit être postérieure ou égale à la date de la facture.', + 'currency.required' => 'La devise est obligatoire.', + 'currency.size' => 'La devise doit comporter 3 caractères.', + 'total_ht.required' => 'Le total HT est obligatoire.', + 'total_ht.numeric' => 'Le total HT doit être un nombre.', + 'total_ht.min' => 'Le total HT ne peut pas être négatif.', + 'total_tva.required' => 'Le total TVA est obligatoire.', + 'total_tva.numeric' => 'Le total TVA doit être un nombre.', + 'total_tva.min' => 'Le total TVA ne peut pas être négatif.', + 'total_ttc.required' => 'Le total TTC est obligatoire.', + 'total_ttc.numeric' => 'Le total TTC doit être un nombre.', + 'total_ttc.min' => 'Le total TTC ne peut pas être négatif.', + 'lines.required' => 'Veuillez ajouter au moins une ligne à la facture.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StorePractitionerDocumentRequest.php b/thanasoft-back/app/Http/Requests/StorePractitionerDocumentRequest.php new file mode 100644 index 0000000..2f18457 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StorePractitionerDocumentRequest.php @@ -0,0 +1,55 @@ + + */ + public function rules(): array + { + return [ + 'practitioner_id' => 'required|exists:thanatopractitioners,id', + 'doc_type' => 'required|string|max:191', + 'file_id' => 'nullable|exists:files,id', + 'issue_date' => 'nullable|date', + 'expiry_date' => 'nullable|date|after_or_equal:issue_date', + 'status' => 'nullable|string|max:64', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'practitioner_id.required' => 'Le thanatopractitioner est obligatoire.', + 'practitioner_id.exists' => 'Le thanatopractitioner sélectionné n\'existe pas.', + 'doc_type.required' => 'Le type de document est obligatoire.', + 'doc_type.string' => 'Le type de document doit être une chaîne de caractères.', + 'doc_type.max' => 'Le type de document ne peut pas dépasser :max caractères.', + 'file_id.exists' => 'Le fichier sélectionné n\'existe pas.', + 'issue_date.date' => 'La date de délivrance doit être une date valide.', + 'expiry_date.date' => 'La date d\'expiration doit être une date valide.', + 'expiry_date.after_or_equal' => 'La date d\'expiration doit être égale ou postérieure à la date de délivrance.', + 'status.string' => 'Le statut doit être une chaîne de caractères.', + 'status.max' => 'Le statut ne peut pas dépasser :max caractères.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StorePriceListRequest.php b/thanasoft-back/app/Http/Requests/StorePriceListRequest.php new file mode 100644 index 0000000..bf65d36 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StorePriceListRequest.php @@ -0,0 +1,43 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'required|string|max:191|unique:price_lists,name', + 'valid_from' => 'nullable|date', + 'valid_to' => 'nullable|date|after_or_equal:valid_from', + 'is_default' => 'nullable|boolean', + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Le nom de la liste de prix est obligatoire.', + 'name.unique' => 'Une liste de prix avec ce nom existe déjà.', + 'valid_from.date' => 'La date de début doit être une date valide.', + 'valid_to.date' => 'La date de fin doit être une date valide.', + 'valid_to.after_or_equal' => 'La date de fin doit être postérieure ou égale à la date de début.', + 'is_default.boolean' => 'Le statut par défaut doit être vrai ou faux.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreProductCategoryRequest.php b/thanasoft-back/app/Http/Requests/StoreProductCategoryRequest.php new file mode 100644 index 0000000..61480de --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreProductCategoryRequest.php @@ -0,0 +1,33 @@ +|string> + */ + public function rules(): array + { + return [ + 'code' => 'required|string|max:64|unique:product_categories,code', + 'name' => 'required|string|max:191', + 'description' => 'nullable|string', + 'parent_id' => 'nullable|exists:product_categories,id', + 'intervention' => 'boolean', + 'active' => 'boolean', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreProductRequest.php b/thanasoft-back/app/Http/Requests/StoreProductRequest.php new file mode 100644 index 0000000..1e293cb --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreProductRequest.php @@ -0,0 +1,89 @@ +|string> + */ + public function rules(): array + { + return [ + 'nom' => 'required|string|max:255', + 'reference' => 'required|string|max:100|unique:products,reference', + 'categorie_id' => 'required|exists:product_categories,id', + 'fabricant' => 'nullable|string|max:191', + 'stock_actuel' => 'required|numeric|min:0', + 'stock_minimum' => 'required|numeric|min:0', + 'unite' => 'required|string|max:50', + 'prix_unitaire' => 'required|numeric|min:0', + 'date_expiration' => 'nullable|date|after:today', + 'numero_lot' => 'nullable|string|max:100', + 'conditionnement_nom' => 'nullable|string|max:191', + 'conditionnement_quantite' => 'nullable|numeric|min:0', + 'conditionnement_unite' => 'nullable|string|max:50', + 'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048', + 'remove_image' => 'nullable|boolean', + 'fiche_technique_url' => 'nullable|url|max:500', + 'fournisseur_id' => 'nullable|exists:fournisseurs,id', + ]; + } + + public function messages(): array + { + return [ + 'nom.required' => 'Le nom du produit est obligatoire.', + 'nom.string' => 'Le nom du produit doit être une chaîne de caractères.', + 'nom.max' => 'Le nom du produit ne peut pas dépasser 255 caractères.', + 'reference.required' => 'La référence du produit est obligatoire.', + 'reference.string' => 'La référence du produit doit être une chaîne de caractères.', + 'reference.max' => 'La référence du produit ne peut pas dépasser 100 caractères.', + 'reference.unique' => 'Cette référence de produit existe déjà.', + 'categorie_id.required' => 'La catégorie est obligatoire.', + 'categorie_id.exists' => 'La catégorie sélectionnée n\'existe pas.', + 'fabricant.string' => 'Le fabricant doit être une chaîne de caractères.', + 'fabricant.max' => 'Le fabricant ne peut pas dépasser 191 caractères.', + 'stock_actuel.required' => 'Le stock actuel est obligatoire.', + 'stock_actuel.numeric' => 'Le stock actuel doit être un nombre.', + 'stock_actuel.min' => 'Le stock actuel doit être supérieur ou égal à 0.', + 'stock_minimum.required' => 'Le stock minimum est obligatoire.', + 'stock_minimum.numeric' => 'Le stock minimum doit être un nombre.', + 'stock_minimum.min' => 'Le stock minimum doit être supérieur ou égal à 0.', + 'unite.required' => 'L\'unité est obligatoire.', + 'unite.string' => 'L\'unité doit être une chaîne de caractères.', + 'unite.max' => 'L\'unité ne peut pas dépasser 50 caractères.', + 'prix_unitaire.required' => 'Le prix unitaire est obligatoire.', + 'prix_unitaire.numeric' => 'Le prix unitaire doit être un nombre.', + 'prix_unitaire.min' => 'Le prix unitaire doit être supérieur ou égal à 0.', + 'date_expiration.date' => 'La date d\'expiration doit être une date valide.', + 'date_expiration.after' => 'La date d\'expiration doit être postérieure à aujourd\'hui.', + 'numero_lot.string' => 'Le numéro de lot doit être une chaîne de caractères.', + 'numero_lot.max' => 'Le numéro de lot ne peut pas dépasser 100 caractères.', + 'conditionnement_nom.string' => 'Le nom du conditionnement doit être une chaîne de caractères.', + 'conditionnement_nom.max' => 'Le nom du conditionnement ne peut pas dépasser 191 caractères.', + 'conditionnement_quantite.numeric' => 'La quantité du conditionnement doit être un nombre.', + 'conditionnement_quantite.min' => 'La quantité du conditionnement doit être supérieure ou égal à 0.', + 'conditionnement_unite.string' => 'L\'unité du conditionnement doit être une chaîne de caractères.', + 'conditionnement_unite.max' => 'L\'unité du conditionnement ne peut pas dépasser 50 caractères.', + 'image.image' => 'Le fichier doit être une image valide.', + 'image.mimes' => 'L\'image doit être de type: jpeg, png, jpg, gif ou svg.', + 'image.max' => 'L\'image ne peut pas dépasser 2MB.', + 'fiche_technique_url.url' => 'L\'URL de la fiche technique doit être une URL valide.', + 'fiche_technique_url.max' => 'L\'URL de la fiche technique ne peut pas dépasser 500 caractères.', + 'fournisseur_id.exists' => 'Le fournisseur sélectionné n\'existe pas.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StorePurchaseOrderRequest.php b/thanasoft-back/app/Http/Requests/StorePurchaseOrderRequest.php new file mode 100644 index 0000000..f3ff1ee --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StorePurchaseOrderRequest.php @@ -0,0 +1,80 @@ +|string> + */ + public function rules(): array + { + return [ + 'fournisseur_id' => ['required', 'exists:fournisseurs,id'], + 'po_number' => ['nullable', 'string', 'max:191', 'unique:purchase_orders,po_number'], + 'status' => ['nullable', 'in:brouillon,confirmee,livree,facturee,annulee'], + 'order_date' => ['nullable', 'date'], + 'expected_date' => ['nullable', 'date'], + 'currency' => ['nullable', 'string', 'size:3'], + 'total_ht' => ['required', 'numeric', 'min:0'], + 'total_tva' => ['required', 'numeric', 'min:0'], + 'total_ttc' => ['required', 'numeric', 'min:0'], + 'notes' => ['nullable', 'string'], + 'delivery_address' => ['nullable', 'string'], + 'lines' => ['required', 'array', 'min:1'], + 'lines.*.product_id' => ['nullable', 'exists:products,id'], + 'lines.*.description' => ['required', 'string'], + 'lines.*.quantity' => ['required', 'numeric', 'min:0.001'], + 'lines.*.unit_price' => ['required', 'numeric', 'min:0'], + 'lines.*.tva_rate' => ['required', 'numeric', 'min:0'], + 'lines.*.discount_pct' => ['nullable', 'numeric', 'min:0', 'max:100'], + 'lines.*.total_ht' => ['required', 'numeric', 'min:0'], + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'fournisseur_id.required' => 'Le fournisseur est obligatoire.', + 'fournisseur_id.exists' => 'Le fournisseur sélectionné est invalide.', + 'po_number.unique' => 'Ce numéro de commande existe déjà.', + 'order_date.date' => 'La date de commande n\'est pas valide.', + 'expected_date.date' => 'La date de livraison prévue n\'est pas valide.', + 'status.in' => 'Le statut sélectionné est invalide.', + 'total_ht.required' => 'Le total HT est obligatoire.', + 'total_tva.required' => 'Le total TVA est obligatoire.', + 'total_ttc.required' => 'Le total TTC est obligatoire.', + 'lines.required' => 'Au moins une ligne d\'article est requise.', + 'lines.array' => 'Les lignes doivent être un tableau.', + 'lines.min' => 'Vous devez ajouter au moins une ligne d\'article.', + 'lines.*.description.required' => 'La désignation est obligatoire pour toutes les lignes.', + 'lines.*.quantity.required' => 'La quantité est obligatoire.', + 'lines.*.quantity.numeric' => 'La quantité doit être un nombre.', + 'lines.*.quantity.min' => 'La quantité doit être supérieure à 0.', + 'lines.*.unit_price.required' => 'Le prix unitaire est obligatoire.', + 'lines.*.unit_price.numeric' => 'Le prix unitaire doit être un nombre.', + 'lines.*.unit_price.min' => 'Le prix unitaire doit être positif.', + 'lines.*.tva_rate.required' => 'Le taux de TVA est obligatoire.', + 'lines.*.total_ht.required' => 'Le total HT de la ligne est obligatoire.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreQuoteLineRequest.php b/thanasoft-back/app/Http/Requests/StoreQuoteLineRequest.php new file mode 100644 index 0000000..51a8ee7 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreQuoteLineRequest.php @@ -0,0 +1,39 @@ +|string> + */ + public function rules(): array + { + return [ + 'quote_id' => 'required|exists:quotes,id', + 'product_id' => 'nullable|exists:products,id', + 'packaging_id' => 'nullable|exists:product_packagings,id', + 'packages_qty' => 'nullable|numeric|min:0', + 'units_qty' => 'nullable|numeric|min:0', + 'description' => 'required|string', + 'qty_base' => 'nullable|numeric|min:0', + 'unit_price' => 'required|numeric|min:0', + 'unit_price_per_package' => 'nullable|numeric|min:0', + 'tva_rate_id' => 'nullable|exists:tva_rates,id', + 'discount_pct' => 'required|numeric|min:0|max:100', + 'total_ht' => 'required|numeric|min:0', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreQuoteRequest.php b/thanasoft-back/app/Http/Requests/StoreQuoteRequest.php new file mode 100644 index 0000000..83cb3d2 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreQuoteRequest.php @@ -0,0 +1,96 @@ +|string> + */ + public function rules(): array + { + return [ + 'client_id' => 'nullable|exists:clients,id', + 'group_id' => 'nullable|exists:client_groups,id', + 'status' => 'required|in:brouillon,envoye,accepte,refuse,expire,annule', + 'quote_date' => 'required|date', + 'valid_until' => 'nullable|date|after_or_equal:quote_date', + 'currency' => 'required|string|size:3', + 'total_ht' => 'required|numeric|min:0', + 'total_tva' => 'required|numeric|min:0', + 'total_ttc' => 'required|numeric|min:0', + 'lines' => 'required|array|min:1', + 'lines.*.product_id' => 'nullable|exists:products,id', + 'lines.*.packaging_id' => 'nullable|exists:product_packagings,id', + 'lines.*.packages_qty' => 'nullable|numeric|min:0', + 'lines.*.units_qty' => 'nullable|numeric|min:0', + 'lines.*.description' => 'required|string', + 'lines.*.qty_base' => 'nullable|numeric|min:0', + 'lines.*.unit_price' => 'required|numeric|min:0', + 'lines.*.unit_price_per_package' => 'nullable|numeric|min:0', + 'lines.*.tva_rate_id' => 'nullable|exists:tva_rates,id', + 'lines.*.discount_pct' => 'required|numeric|min:0|max:100', + 'lines.*.total_ht' => 'required|numeric|min:0', + ]; + } + + public function withValidator($validator): void + { + $validator->after(function ($validator) { + $hasClient = filled($this->input('client_id')); + $hasGroup = filled($this->input('group_id')); + + if (! $hasClient && ! $hasGroup) { + $message = 'Un client ou un groupe de clients est obligatoire.'; + + $validator->errors()->add('client_id', $message); + $validator->errors()->add('group_id', $message); + } + + if ($hasClient && $hasGroup) { + $message = 'Sélectionnez soit un client, soit un groupe de clients.'; + + $validator->errors()->add('client_id', $message); + $validator->errors()->add('group_id', $message); + } + }); + } + + public function messages(): array + { + return [ + 'client_id.nullable' => 'Le client sélectionné est invalide.', + 'client_id.exists' => 'Le client sélectionné est invalide.', + 'group_id.exists' => 'Le groupe sélectionné est invalide.', + 'status.required' => 'Le statut est obligatoire.', + 'status.in' => 'Le statut sélectionné est invalide.', + 'quote_date.required' => 'La date du devis est obligatoire.', + 'quote_date.date' => 'La date du devis n\'est pas valide.', + 'valid_until.date' => 'La date de validité n\'est pas valide.', + 'valid_until.after_or_equal' => 'La date de validité doit être postérieure ou égale à la date du devis.', + 'currency.required' => 'La devise est obligatoire.', + 'currency.size' => 'La devise doit comporter 3 caractères.', + 'total_ht.required' => 'Le total HT est obligatoire.', + 'total_ht.numeric' => 'Le total HT doit être un nombre.', + 'total_ht.min' => 'Le total HT ne peut pas être négatif.', + 'total_tva.required' => 'Le total TVA est obligatoire.', + 'total_tva.numeric' => 'Le total TVA doit être un nombre.', + 'total_tva.min' => 'Le total TVA ne peut pas être négatif.', + 'total_ttc.required' => 'Le total TTC est obligatoire.', + 'total_ttc.numeric' => 'Le total TTC doit être un nombre.', + 'total_ttc.min' => 'Le total TTC ne peut pas être négatif.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreStockItemRequest.php b/thanasoft-back/app/Http/Requests/StoreStockItemRequest.php new file mode 100644 index 0000000..b9b1b12 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreStockItemRequest.php @@ -0,0 +1,48 @@ +|string> + */ + public function rules(): array + { + return [ + 'product_id' => 'required|exists:products,id', + 'warehouse_id' => 'required|exists:warehouses,id', + 'qty_on_hand_base' => 'nullable|numeric|min:0', + 'safety_stock_base' => 'nullable|numeric|min:0', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'product_id.required' => 'Le produit est requis.', + 'product_id.exists' => 'Le produit sélectionné est invalide.', + 'warehouse_id.required' => 'L\'entrepôt est requis.', + 'warehouse_id.exists' => 'L\'entrepôt sélectionné est invalide.', + 'qty_on_hand_base.numeric' => 'La quantité en stock doit être un nombre.', + 'safety_stock_base.numeric' => 'Le stock de sécurité doit être un nombre.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreStockMoveRequest.php b/thanasoft-back/app/Http/Requests/StoreStockMoveRequest.php new file mode 100644 index 0000000..3d69eab --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreStockMoveRequest.php @@ -0,0 +1,58 @@ +|string> + */ + public function rules(): array + { + return [ + 'product_id' => 'required|exists:products,id', + 'from_warehouse_id' => 'nullable|exists:warehouses,id', + 'to_warehouse_id' => 'nullable|exists:warehouses,id', + 'packaging_id' => 'nullable|exists:product_packagings,id', + 'packages_qty' => 'nullable|numeric|min:0', + 'units_qty' => 'nullable|numeric|min:0', + 'qty_base' => 'required|numeric', + 'move_type' => 'required|string|max:64', + 'ref_type' => 'nullable|string|max:64', + 'ref_id' => 'nullable|integer', + 'moved_at' => 'nullable|date', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'product_id.required' => 'Le produit est requis.', + 'product_id.exists' => 'Le produit sélectionné est invalide.', + 'from_warehouse_id.exists' => 'L\'entrepôt de départ est invalide.', + 'to_warehouse_id.exists' => 'L\'entrepôt d\'arrivée est invalide.', + 'packaging_id.exists' => 'Le conditionnement sélectionné est invalide.', + 'qty_base.required' => 'La quantité de base est requise.', + 'qty_base.numeric' => 'La quantité de base doit être un nombre.', + 'move_type.required' => 'Le type de mouvement est requis.', + 'moved_at.date' => 'La date du mouvement n\'est pas valide.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreThanatopractitionerRequest.php b/thanasoft-back/app/Http/Requests/StoreThanatopractitionerRequest.php new file mode 100644 index 0000000..7ad6870 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreThanatopractitionerRequest.php @@ -0,0 +1,57 @@ + + */ + public function rules(): array + { + return [ + 'employee_id' => 'required|exists:employees,id|unique:thanatopractitioners,employee_id', + 'diploma_number' => 'nullable|string|max:191', + 'diploma_date' => 'nullable|date', + 'authorization_number' => 'nullable|string|max:191', + 'authorization_issue_date' => 'nullable|date', + 'authorization_expiry_date' => 'nullable|date|after_or_equal:authorization_issue_date', + 'notes' => 'nullable|string', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'employee_id.required' => 'L\'employé est obligatoire.', + 'employee_id.exists' => 'L\'employé sélectionné n\'existe pas.', + 'employee_id.unique' => 'Cet employé est déjà enregistré comme thanatopractitioner.', + 'diploma_number.string' => 'Le numéro de diplôme doit être une chaîne de caractères.', + 'diploma_number.max' => 'Le numéro de diplôme ne peut pas dépasser :max caractères.', + 'diploma_date.date' => 'La date d\'obtention du diplôme doit être une date valide.', + 'authorization_number.string' => 'Le numéro d\'autorisation doit être une chaîne de caractères.', + 'authorization_number.max' => 'Le numéro d\'autorisation ne peut pas dépasser :max caractères.', + 'authorization_issue_date.date' => 'La date de délivrance de l\'autorisation doit être une date valide.', + 'authorization_expiry_date.date' => 'La date d\'expiration de l\'autorisation doit être une date valide.', + 'authorization_expiry_date.after_or_equal' => 'La date d\'expiration doit être égale ou postérieure à la date de délivrance.', + 'notes.string' => 'Les notes doivent être une chaîne de caractères.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreTvaRateRequest.php b/thanasoft-back/app/Http/Requests/StoreTvaRateRequest.php new file mode 100644 index 0000000..d9acb93 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreTvaRateRequest.php @@ -0,0 +1,47 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'required|string|max:50', + 'rate' => 'required|numeric|min:0|max:999.99', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'name.required' => 'Le nom du taux de TVA est requis.', + 'name.string' => 'Le nom doit être une chaîne de caractères.', + 'name.max' => 'Le nom ne peut pas dépasser 50 caractères.', + 'rate.required' => 'Le taux de TVA est requis.', + 'rate.numeric' => 'Le taux doit être un nombre.', + 'rate.min' => 'Le taux ne peut pas être négatif.', + 'rate.max' => 'Le taux ne peut pas dépasser 999.99.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreUserRequest.php b/thanasoft-back/app/Http/Requests/StoreUserRequest.php new file mode 100644 index 0000000..a8f3fce --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreUserRequest.php @@ -0,0 +1,32 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'], + 'roles' => ['nullable', 'array'], + 'roles.*' => ['string', 'max:100'], + 'permissions' => ['nullable', 'array'], + 'permissions.*' => ['string', 'max:150'], + 'password' => ['nullable', 'string', Password::min(8)], + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreVehicleRequest.php b/thanasoft-back/app/Http/Requests/StoreVehicleRequest.php new file mode 100644 index 0000000..a94f251 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreVehicleRequest.php @@ -0,0 +1,33 @@ + ['nullable', 'string', 'max:255'], + 'photo_file_url' => ['nullable', 'string', 'max:2048'], + 'photo_mime_type' => ['nullable', 'string', 'max:100'], + 'photo_size' => ['nullable', 'integer', 'min:0'], + 'brand' => ['required', 'string', 'max:255'], + 'model' => ['required', 'string', 'max:255'], + 'registration_number' => ['required', 'string', 'max:255', 'unique:vehicles,registration_number'], + 'vehicle_type' => ['nullable', Rule::in(['hearse', 'transport_vehicle', 'utility', 'sedan'])], + 'fuel_type' => ['nullable', Rule::in(['diesel', 'petrol', 'electric', 'hybrid'])], + 'year' => ['nullable', 'integer', 'min:1900', 'max:2100'], + 'primary_user_id' => ['nullable', 'exists:employees,id'], + 'status' => ['nullable', Rule::in(['active', 'maintenance', 'out_of_service'])], + 'notes' => ['nullable', 'string'], + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreWarehouseRequest.php b/thanasoft-back/app/Http/Requests/StoreWarehouseRequest.php new file mode 100644 index 0000000..a2c9c9a --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreWarehouseRequest.php @@ -0,0 +1,52 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'required|string|max:191', + 'address_line1' => 'nullable|string|max:255', + 'address_line2' => 'nullable|string|max:255', + 'postal_code' => 'nullable|string|max:20', + 'city' => 'nullable|string|max:191', + 'country_code' => 'nullable|string|size:2', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'name.required' => 'Le nom de l\'entrepôt est requis.', + 'name.string' => 'Le nom doit être une chaîne de caractères.', + 'name.max' => 'Le nom ne peut pas dépasser 191 caractères.', + 'address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.', + 'address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.', + 'postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.', + 'city.max' => 'La ville ne peut pas dépasser 191 caractères.', + 'country_code.size' => 'Le code pays doit comporter exactement 2 caractères.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateAvoirRequest.php b/thanasoft-back/app/Http/Requests/UpdateAvoirRequest.php new file mode 100644 index 0000000..2fa4248 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateAvoirRequest.php @@ -0,0 +1,27 @@ + 'sometimes|required|in:brouillon,emis,applique,annule', + 'avoir_date' => 'sometimes|required|date', + 'due_date' => 'nullable|date|after_or_equal:avoir_date', + 'refund_status' => 'nullable|in:non_rembourse,en_cours,partiellement_rembourse,rembourse,compense', + 'refund_date' => 'nullable|date', + 'refund_method' => 'nullable|in:virement,cheque,carte_credit,compensation_future,autre', + 'reason_description' => 'nullable|string', + 'notes' => 'nullable|string', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateClientGroupRequest.php b/thanasoft-back/app/Http/Requests/UpdateClientGroupRequest.php new file mode 100644 index 0000000..0f16264 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateClientGroupRequest.php @@ -0,0 +1,57 @@ +|string> + */ + public function rules(): array + { + $routeClientGroup = $this->route('client_group'); + $clientGroupId = is_object($routeClientGroup) + ? $routeClientGroup->id + : ($routeClientGroup ?? $this->route('id')); + + return [ + 'name' => [ + 'required', + 'string', + 'max:191', + Rule::unique('client_groups', 'name')->ignore($clientGroupId) + ], + 'description' => 'nullable|string', + 'client_ids' => 'sometimes|array', + 'client_ids.*' => 'integer|distinct|exists:clients,id', + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Le nom du groupe est obligatoire.', + 'name.string' => 'Le nom du groupe doit être une chaîne de caractères.', + 'name.max' => 'Le nom du groupe ne peut pas dépasser 191 caractères.', + 'name.unique' => 'Un groupe avec ce nom existe déjà.', + 'description.string' => 'La description doit être une chaîne de caractères.', + 'client_ids.array' => 'La liste des clients doit être un tableau.', + 'client_ids.*.integer' => 'Chaque ID client doit être un entier.', + 'client_ids.*.distinct' => 'Un client ne peut pas être sélectionné plusieurs fois.', + 'client_ids.*.exists' => 'Un ou plusieurs clients sélectionnés sont introuvables.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateClientLocationRequest.php b/thanasoft-back/app/Http/Requests/UpdateClientLocationRequest.php new file mode 100644 index 0000000..66606cf --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateClientLocationRequest.php @@ -0,0 +1,56 @@ +|string> + */ + public function rules(): array + { + return [ + 'client_id' => 'required|exists:clients,id', + 'name' => 'nullable|string|max:191', + 'address_line1' => 'nullable|string|max:255', + 'address_line2' => 'nullable|string|max:255', + 'postal_code' => 'nullable|string|max:20', + 'city' => 'nullable|string|max:191', + 'country_code' => 'nullable|string|size:2', + 'gps_lat' => 'nullable|numeric|between:-90,90', + 'gps_lng' => 'nullable|numeric|between:-180,180', + 'is_default' => 'boolean', + ]; + } + + public function messages(): array + { + return [ + 'client_id.required' => 'Le client est obligatoire.', + 'client_id.exists' => 'Le client sélectionné n\'existe pas.', + 'name.max' => 'Le nom ne peut pas dépasser 191 caractères.', + 'address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.', + 'address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.', + 'postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.', + 'city.max' => 'La ville ne peut pas dépasser 191 caractères.', + 'country_code.size' => 'Le code pays doit contenir 2 caractères.', + 'gps_lat.numeric' => 'La latitude doit être un nombre.', + 'gps_lat.between' => 'La latitude doit être comprise entre -90 et 90.', + 'gps_lng.numeric' => 'La longitude doit être un nombre.', + 'gps_lng.between' => 'La longitude doit être comprise entre -180 et 180.', + 'is_default.boolean' => 'Le statut par défaut doit être vrai ou faux.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateClientRequest.php b/thanasoft-back/app/Http/Requests/UpdateClientRequest.php new file mode 100644 index 0000000..ac94c1a --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateClientRequest.php @@ -0,0 +1,71 @@ +|string> + */ + public function rules(): array + { + return [ + + 'name' => 'required|string|max:255', + 'vat_number' => 'nullable|string|max:32', + 'siret' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:191', + 'phone' => 'nullable|string|max:50', + 'billing_address_line1' => 'nullable|string|max:255', + 'billing_address_line2' => 'nullable|string|max:255', + 'billing_postal_code' => 'nullable|string|max:20', + 'billing_city' => 'nullable|string|max:191', + 'billing_country_code' => 'nullable|string|size:2', + 'group_id' => 'nullable|exists:client_groups,id', + 'notes' => 'nullable|string', + 'is_active' => 'boolean', + 'is_parent' => 'boolean|nullable', + 'parent_id' => 'nullable|exists:clients,id', + 'default_tva_rate_id' => 'nullable|exists:tva_rates,id', + 'avatar' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048', + ]; + } + + public function messages(): array + { + return [ + 'company_id.required' => 'La société est obligatoire.', + 'company_id.exists' => 'La société sélectionnée n\'existe pas.', + 'type.required' => 'Le type de client est obligatoire.', + 'type.in' => 'Le type de client sélectionné est invalide.', + 'name.required' => 'Le nom du client est obligatoire.', + 'name.string' => 'Le nom du client doit être une chaîne de caractères.', + 'name.max' => 'Le nom du client ne peut pas dépasser 255 caractères.', + 'vat_number.max' => 'Le numéro de TVA ne peut pas dépasser 32 caractères.', + 'siret.max' => 'Le SIRET ne peut pas dépasser 20 caractères.', + 'email.email' => 'L\'adresse email doit être valide.', + 'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.', + 'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.', + 'billing_address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.', + 'billing_address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.', + 'billing_postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.', + 'billing_city.max' => 'La ville ne peut pas dépasser 191 caractères.', + 'billing_country_code.size' => 'Le code pays doit contenir 2 caractères.', + 'group_id.exists' => 'Le groupe de clients sélectionné n\'existe pas.', + 'is_active.boolean' => 'Le statut actif doit être vrai ou faux.', + 'default_tva_rate_id.exists' => 'Le taux de TVA sélectionné n\'existe pas.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateContactRequest.php b/thanasoft-back/app/Http/Requests/UpdateContactRequest.php new file mode 100644 index 0000000..f960bf0 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateContactRequest.php @@ -0,0 +1,71 @@ +|string> + */ + public function rules(): array + { + return [ + 'client_id' => 'nullable|exists:clients,id', + 'fournisseur_id' => 'nullable|exists:fournisseurs,id', + 'first_name' => 'nullable|string|max:191', + 'last_name' => 'nullable|string|max:191', + 'email' => 'nullable|email|max:191', + 'phone' => 'nullable|string|max:50', + 'role' => 'nullable|string|max:191', + ]; + } + + public function messages(): array + { + return [ + 'client_id.exists' => 'Le client sélectionné n\'existe pas.', + 'fournisseur_id.exists' => 'Le fournisseur sélectionné n\'existe pas.', + 'first_name.string' => 'Le prénom doit être une chaîne de caractères.', + 'first_name.max' => 'Le prénom ne peut pas dépasser 191 caractères.', + 'last_name.string' => 'Le nom doit être une chaîne de caractères.', + 'last_name.max' => 'Le nom ne peut pas dépasser 191 caractères.', + 'email.email' => 'L\'adresse email doit être valide.', + 'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.', + 'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.', + 'role.max' => 'Le rôle ne peut pas dépasser 191 caractères.', + ]; + } + + + public function withValidator($validator) + { + $validator->after(function ($validator) { + // At least one of client_id or fournisseur_id must be provided + if (empty($this->client_id) && empty($this->fournisseur_id)) { + $validator->errors()->add( + 'general', + 'Le contact doit être associé à un client ou un fournisseur.' + ); + } + + if (empty($this->first_name) && empty($this->last_name) && empty($this->email) && empty($this->phone)) { + $validator->errors()->add( + 'general', + 'Au moins un champ (prénom, nom, email ou téléphone) doit être renseigné.' + ); + } + }); + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateConvoyRequest.php b/thanasoft-back/app/Http/Requests/UpdateConvoyRequest.php new file mode 100644 index 0000000..240064e --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateConvoyRequest.php @@ -0,0 +1,51 @@ + ['sometimes', 'required', 'exists:deceased,id'], + 'client_id' => ['nullable', 'exists:clients,id'], + 'vehicle_id' => ['nullable', 'exists:vehicles,id'], + 'mission_title' => ['nullable', 'string', 'max:255'], + 'convoy_type' => ['nullable', Rule::in(['local', 'national', 'international'])], + 'transport_mode' => ['nullable', Rule::in(['road', 'air', 'sea', 'rail'])], + 'status' => ['nullable', Rule::in(['planned', 'in_progress', 'completed', 'cancelled'])], + 'planned_start_at' => ['sometimes', 'required', 'date'], + 'estimated_end_at' => ['nullable', 'date'], + 'family_email' => ['nullable', 'email', 'max:255'], + 'automatic_notifications' => ['nullable', 'boolean'], + 'departure_location_selection_mode' => ['nullable', Rule::in(['place', 'manual'])], + 'departure_location_id' => ['nullable', 'exists:client_locations,id'], + 'departure_name' => ['nullable', 'string', 'max:255'], + 'departure_address' => ['nullable', 'string', 'max:255'], + 'departure_city' => ['nullable', 'string', 'max:255'], + 'departure_postal_code' => ['nullable', 'string', 'max:20'], + 'departure_country_code' => ['nullable', 'string', 'size:2'], + 'departure_latitude' => ['nullable', 'numeric', 'between:-90,90'], + 'departure_longitude' => ['nullable', 'numeric', 'between:-180,180'], + 'departure_additional_details' => ['nullable', 'string'], + 'tab_itinerary' => ['nullable', 'boolean'], + 'tab_legal_documents' => ['nullable', 'boolean'], + 'tab_team_resources' => ['nullable', 'boolean'], + 'tab_procedures' => ['nullable', 'boolean'], + 'tab_cost_tracking' => ['nullable', 'boolean'], + 'tab_fuel' => ['nullable', 'boolean'], + 'tab_ceremony' => ['nullable', 'boolean'], + 'tab_thanatopraxy' => ['nullable', 'boolean'], + 'tab_gps_tracking_steps' => ['nullable', 'boolean'], + 'tab_communication' => ['nullable', 'boolean'], + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateDeceasedDocumentRequest.php b/thanasoft-back/app/Http/Requests/UpdateDeceasedDocumentRequest.php new file mode 100644 index 0000000..cae38d2 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateDeceasedDocumentRequest.php @@ -0,0 +1,49 @@ + + */ + public function rules(): array + { + return [ + 'deceased_id' => 'sometimes|required|exists:deceased,id', + 'doc_type' => 'sometimes|required|string|max:191', + 'file_id' => 'nullable|exists:files,id', + 'generated_at' => 'nullable|date', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'deceased_id.required' => 'Le défunt est obligatoire.', + 'deceased_id.exists' => 'Le défunt sélectionné n\'existe pas.', + 'doc_type.required' => 'Le type de document est obligatoire.', + 'doc_type.string' => 'Le type de document doit être une chaîne de caractères.', + 'doc_type.max' => 'Le type de document ne peut pas dépasser :max caractères.', + 'file_id.exists' => 'Le fichier sélectionné n\'existe pas.', + 'generated_at.date' => 'La date de génération doit être une date valide.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateDeceasedRequest.php b/thanasoft-back/app/Http/Requests/UpdateDeceasedRequest.php new file mode 100644 index 0000000..dcafcd4 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateDeceasedRequest.php @@ -0,0 +1,49 @@ +|string> + */ + public function rules(): array + { + return [ + 'last_name' => ['sometimes', 'required', 'string', 'max:191'], + 'first_name' => ['nullable', 'string', 'max:191'], + 'birth_date' => ['nullable', 'date'], + 'death_date' => ['nullable', 'date', 'after_or_equal:birth_date'], + 'place_of_death' => ['nullable', 'string', 'max:255'], + 'notes' => ['nullable', 'string'] + ]; + } + + /** + * Get custom error messages for validator errors. + */ + public function messages(): array + { + return [ + 'last_name.required' => 'Le nom de famille est obligatoire.', + 'last_name.max' => 'Le nom de famille ne peut pas dépasser 191 caractères.', + 'first_name.max' => 'Le prénom ne peut pas dépasser 191 caractères.', + 'death_date.after_or_equal' => 'La date de décès doit être postérieure ou égale à la date de naissance.', + 'place_of_death.max' => 'Le lieu de décès ne peut pas dépasser 255 caractères.' + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateEmployeeRequest.php b/thanasoft-back/app/Http/Requests/UpdateEmployeeRequest.php new file mode 100644 index 0000000..c0ecff7 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateEmployeeRequest.php @@ -0,0 +1,67 @@ + + */ + public function rules(): array + { + return [ + 'first_name' => 'nullable|string|max:191', + 'last_name' => 'nullable|string|max:191', + 'email' => [ + 'nullable', + 'email', + 'max:191', + Rule::unique('employees', 'email')->ignore($this->route('employee')) + ], + 'phone' => 'nullable|string|max:50', + 'user_id' => 'nullable|exists:users,id', + 'job_title' => 'nullable|string|max:191', + 'hire_date' => 'nullable|date', + 'active' => 'boolean', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'first_name.required' => 'Le prénom est obligatoire.', + 'first_name.string' => 'Le prénom doit être une chaîne de caractères.', + 'first_name.max' => 'Le prénom ne peut pas dépasser :max caractères.', + 'last_name.required' => 'Le nom de famille est obligatoire.', + 'last_name.string' => 'Le nom de famille doit être une chaîne de caractères.', + 'last_name.max' => 'Le nom de famille ne peut pas dépasser :max caractères.', + 'email.email' => 'L\'adresse email doit être valide.', + 'email.unique' => 'Cette adresse email est déjà utilisée.', + 'phone.string' => 'Le téléphone doit être une chaîne de caractères.', + 'phone.max' => 'Le téléphone ne peut pas dépasser :max caractères.', + 'user_id.exists' => 'L\'utilisateur sélectionné est invalide.', + 'job_title.string' => 'L\'intitulé du poste doit être une chaîne de caractères.', + 'job_title.max' => 'L\'intitulé du poste ne peut pas dépasser :max caractères.', + 'hire_date.date' => 'La date d\'embauche doit être une date valide.', + 'active.boolean' => 'Le statut actif doit être un booléen.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateFileRequest.php b/thanasoft-back/app/Http/Requests/UpdateFileRequest.php new file mode 100644 index 0000000..39cf9c3 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateFileRequest.php @@ -0,0 +1,96 @@ +route('file'); + + // Allow if user owns the file or is admin + return Auth::check() && ( + $file->uploaded_by === Auth::id() || + Auth::user()->hasRole('admin') + ); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + 'file_name' => 'sometimes|string|max:255', + 'description' => 'nullable|string|max:500', + 'tags' => 'nullable|array|max:10', + 'tags.*' => 'string|max:50', + 'is_public' => 'boolean', + 'category' => 'sometimes|string|in:devis,facture,contrat,document,image,autre', + 'client_id' => 'nullable|integer|exists:clients,id', + 'subcategory' => 'nullable|string|max:100', + ]; + } + + /** + * Get custom attributes for validator errors. + */ + public function attributes(): array + { + return [ + 'file_name' => 'nom du fichier', + 'description' => 'description', + 'tags' => 'étiquettes', + 'is_public' => 'visibilité publique', + 'category' => 'catégorie', + 'client_id' => 'client', + 'subcategory' => 'sous-catégorie', + ]; + } + + /** + * Get the error messages for the defined validation rules. + */ + public function messages(): array + { + return [ + 'file_name.string' => 'Le nom du fichier doit être une chaîne de caractères.', + 'file_name.max' => 'Le nom du fichier ne peut pas dépasser 255 caractères.', + 'description.string' => 'La description doit être une chaîne de caractères.', + 'description.max' => 'La description ne peut pas dépasser 500 caractères.', + 'tags.array' => 'Les étiquettes doivent être un tableau.', + 'tags.max' => 'Vous ne pouvez pas ajouter plus de 10 étiquettes.', + 'tags.*.string' => 'Chaque étiquette doit être une chaîne de caractères.', + 'tags.*.max' => 'Chaque étiquette ne peut pas dépasser 50 caractères.', + 'is_public.boolean' => 'La visibilité publique doit être vrai ou faux.', + 'category.string' => 'La catégorie doit être une chaîne de caractères.', + 'category.in' => 'La catégorie sélectionnée n\'est pas valide.', + 'client_id.exists' => 'Le client sélectionné n\'existe pas.', + 'subcategory.string' => 'La sous-catégorie doit être une chaîne de caractères.', + 'subcategory.max' => 'La sous-catégorie ne peut pas dépasser 100 caractères.', + ]; + } + + /** + * Prepare the data for validation. + */ + protected function prepareForValidation(): void + { + // Only merge fields that are present in the request + $data = []; + + if ($this->has('is_public')) { + $data['is_public'] = $this->boolean('is_public'); + } + + $this->merge($data); + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateFournisseurRequest.php b/thanasoft-back/app/Http/Requests/UpdateFournisseurRequest.php new file mode 100644 index 0000000..32923d9 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateFournisseurRequest.php @@ -0,0 +1,59 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'sometimes|required|string|max:255', + 'vat_number' => 'nullable|string|max:32', + 'siret' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:191', + 'phone' => 'nullable|string|max:50', + 'billing_address_line1' => 'nullable|string|max:255', + 'billing_address_line2' => 'nullable|string|max:255', + 'billing_postal_code' => 'nullable|string|max:20', + 'billing_city' => 'nullable|string|max:191', + 'billing_country_code' => 'nullable|string|size:2', + 'notes' => 'nullable|string', + 'is_active' => 'boolean', + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Le nom du fournisseur est obligatoire.', + 'name.string' => 'Le nom du fournisseur doit être une chaîne de caractères.', + 'name.max' => 'Le nom du fournisseur ne peut pas dépasser 255 caractères.', + 'vat_number.max' => 'Le numéro de TVA ne peut pas dépasser 32 caractères.', + 'siret.max' => 'Le SIRET ne peut pas dépasser 20 caractères.', + 'email.email' => 'L\'adresse email doit être valide.', + 'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.', + 'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.', + 'billing_address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.', + 'billing_address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.', + 'billing_postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.', + 'billing_city.max' => 'La ville ne peut pas dépasser 191 caractères.', + 'billing_country_code.size' => 'Le code pays doit contenir 2 caractères.', + 'is_active.boolean' => 'Le statut actif doit être vrai ou faux.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateGoodsReceiptRequest.php b/thanasoft-back/app/Http/Requests/UpdateGoodsReceiptRequest.php new file mode 100644 index 0000000..a7566c2 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateGoodsReceiptRequest.php @@ -0,0 +1,75 @@ +|string> + */ + public function rules(): array + { + return [ + 'purchase_order_id' => 'sometimes|exists:purchase_orders,id', + 'warehouse_id' => 'sometimes|exists:warehouses,id', + 'receipt_number' => 'sometimes|string|max:191', + 'receipt_date' => 'sometimes|date', + 'status' => 'nullable|in:draft,posted', + 'notes' => 'nullable|string', + 'lines' => 'nullable|array', + 'lines.*.product_id' => 'required_with:lines|exists:products,id', + 'lines.*.packaging_id' => 'nullable|exists:product_packagings,id', + 'lines.*.packages_qty_received' => 'nullable|numeric|min:0', + 'lines.*.units_qty_received' => 'nullable|numeric|min:0', + 'lines.*.qty_received_base' => 'nullable|numeric|min:0', + 'lines.*.unit_price' => 'nullable|numeric|min:0', + 'lines.*.unit_price_per_package' => 'nullable|numeric|min:0', + 'lines.*.tva_rate_id' => 'nullable|exists:tva_rates,id', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'purchase_order_id.exists' => 'La commande fournisseur spécifiée n\'existe pas.', + 'warehouse_id.exists' => 'L\'entrepôt spécifié n\'existe pas.', + 'receipt_number.string' => 'Le numéro de réception doit être une chaîne de caractères.', + 'receipt_number.max' => 'Le numéro de réception ne peut pas dépasser 191 caractères.', + 'receipt_date.date' => 'La date de réception doit être une date valide.', + 'status.in' => 'Le statut doit être "draft" ou "posted".', + 'notes.string' => 'Les notes doivent être une chaîne de caractères.', + 'lines.array' => 'Les lignes doivent être un tableau.', + 'lines.*.product_id.required_with' => 'Le produit est requis pour chaque ligne.', + 'lines.*.product_id.exists' => 'Le produit spécifié dans une ligne n\'existe pas.', + 'lines.*.packaging_id.exists' => 'Le conditionnement spécifié dans une ligne n\'existe pas.', + 'lines.*.packages_qty_received.numeric' => 'La quantité de colis doit être un nombre.', + 'lines.*.packages_qty_received.min' => 'La quantité de colis ne peut pas être négative.', + 'lines.*.units_qty_received.numeric' => 'La quantité d\'unités doit être un nombre.', + 'lines.*.units_qty_received.min' => 'La quantité d\'unités ne peut pas être négative.', + 'lines.*.qty_received_base.numeric' => 'La quantité de base doit être un nombre.', + 'lines.*.qty_received_base.min' => 'La quantité de base ne peut pas être négative.', + 'lines.*.unit_price.numeric' => 'Le prix unitaire doit être un nombre.', + 'lines.*.unit_price.min' => 'Le prix unitaire ne peut pas être négatif.', + 'lines.*.unit_price_per_package.numeric' => 'Le prix par colis doit être un nombre.', + 'lines.*.unit_price_per_package.min' => 'Le prix par colis ne peut pas être négatif.', + 'lines.*.tva_rate_id.exists' => 'Le taux de TVA spécifié dans une ligne n\'existe pas.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateInterventionRequest.php b/thanasoft-back/app/Http/Requests/UpdateInterventionRequest.php new file mode 100644 index 0000000..6a3c1c5 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateInterventionRequest.php @@ -0,0 +1,84 @@ +|string> + */ + public function rules(): array + { + return [ + 'client_id' => ['sometimes', 'required', 'exists:clients,id'], + 'deceased_id' => ['nullable', 'exists:deceased,id'], + 'order_giver' => ['nullable', 'string', 'max:255'], + 'location_id' => ['nullable', 'exists:client_locations,id'], + 'product_id' => ['nullable', 'exists:products,id'], + 'type' => ['sometimes', 'required', Rule::in([ + 'thanatopraxie', + 'toilette_mortuaire', + 'exhumation', + 'retrait_pacemaker', + 'retrait_bijoux', + 'autre' + ])], + 'scheduled_at' => ['nullable', 'date_format:Y-m-d H:i:s'], + 'duration_min' => ['nullable', 'integer', 'min:0'], + 'status' => ['sometimes', Rule::in([ + 'demande', + 'planifie', + 'en_cours', + 'termine', + 'annule' + ])], + 'practitioners' => ['nullable', 'array'], + 'practitioners.*' => ['exists:thanatopractitioners,id'], + 'principal_practitioner_id' => ['nullable', 'exists:thanatopractitioners,id'], + 'assistant_practitioner_ids' => ['nullable', 'array'], + 'assistant_practitioner_ids.*' => ['exists:thanatopractitioners,id'], + 'notes' => ['nullable', 'string'], + 'created_by' => ['nullable', 'exists:users,id'] + ]; + } + + /** + * Get custom error messages for validator errors. + */ + public function messages(): array + { + return [ + 'client_id.required' => 'Le client est obligatoire.', + 'client_id.exists' => 'Le client sélectionné est invalide.', + 'deceased_id.exists' => 'Le défunt sélectionné est invalide.', + 'order_giver.max' => 'Le donneur d\'ordre ne peut pas dépasser 255 caractères.', + 'location_id.exists' => 'Le lieu sélectionné est invalide.', + 'type.required' => 'Le type d\'intervention est obligatoire.', + 'type.in' => 'Le type d\'intervention est invalide.', + 'scheduled_at.date_format' => 'Le format de la date programmée est invalide.', + 'duration_min.integer' => 'La durée doit être un nombre entier.', + 'duration_min.min' => 'La durée ne peut pas être négative.', + 'status.in' => 'Le statut de l\'intervention est invalide.', + 'practitioners.array' => 'Les praticiens doivent être un tableau.', + 'practitioners.*.exists' => 'Un des praticiens sélectionnés est invalide.', + 'principal_practitioner_id.exists' => 'Le praticien principal sélectionné est invalide.', + 'assistant_practitioner_ids.array' => 'Les praticiens assistants doivent être un tableau.', + 'assistant_practitioner_ids.*.exists' => 'Un des praticiens assistants est invalide.', + 'created_by.exists' => 'L\'utilisateur créateur est invalide.' + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateInvoiceRequest.php b/thanasoft-back/app/Http/Requests/UpdateInvoiceRequest.php new file mode 100644 index 0000000..a996b5a --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateInvoiceRequest.php @@ -0,0 +1,93 @@ +|string> + */ + public function rules(): array + { + $invoiceId = $this->route('invoice'); + + return [ + 'client_id' => 'nullable|exists:clients,id', + 'group_id' => 'nullable|exists:client_groups,id', + 'source_quote_id' => 'nullable|exists:quotes,id', + 'invoice_number' => 'sometimes|string|max:191|unique:invoices,invoice_number,' . $invoiceId, + 'status' => 'sometimes|in:brouillon,emise,envoyee,partiellement_payee,payee,echue,annulee,avoir', + 'invoice_date' => 'sometimes|date', + 'due_date' => 'nullable|date|after_or_equal:invoice_date', + 'currency' => 'sometimes|string|size:3', + 'total_ht' => 'sometimes|numeric|min:0', + 'total_tva' => 'sometimes|numeric|min:0', + 'total_ttc' => 'sometimes|numeric|min:0', + 'e_invoicing_channel_id' => 'nullable|exists:e_invoicing_channels,id', + 'e_invoice_status' => 'nullable|in:cree,transmis,accepte,refuse,en_litige,acquitte,archive', + ]; + } + + public function messages(): array + { + return [ + 'client_id.exists' => 'Le client sélectionné est invalide.', + 'group_id.exists' => 'Le groupe sélectionné est invalide.', + 'source_quote_id.exists' => 'Le devis source est invalide.', + 'invoice_number.string' => 'Le numéro de facture doit être une chaîne de caractères.', + 'invoice_number.max' => 'Le numéro de facture ne doit pas dépasser 191 caractères.', + 'invoice_number.unique' => 'Ce numéro de facture existe déjà.', + 'status.in' => 'Le statut sélectionné est invalide.', + 'invoice_date.date' => 'La date de la facture n\'est pas valide.', + 'due_date.date' => 'La date d\'échéance n\'est pas valide.', + 'due_date.after_or_equal' => 'La date d\'échéance doit être postérieure ou égale à la date de la facture.', + 'currency.size' => 'La devise doit comporter 3 caractères.', + 'total_ht.numeric' => 'Le total HT doit être un nombre.', + 'total_ht.min' => 'Le total HT ne peut pas être négatif.', + 'total_tva.numeric' => 'Le total TVA doit être un nombre.', + 'total_tva.min' => 'Le total TVA ne peut pas être négatif.', + 'total_ttc.numeric' => 'Le total TTC doit être un nombre.', + 'total_ttc.min' => 'Le total TTC ne peut pas être négatif.', + 'e_invoicing_channel_id.exists' => 'Le canal de facturation électronique est invalide.', + 'e_invoice_status.in' => 'Le statut de facturation électronique est invalide.', + ]; + } + + public function withValidator($validator): void + { + $validator->after(function ($validator) { + if (! $this->hasAny(['client_id', 'group_id'])) { + return; + } + + $hasClient = filled($this->input('client_id')); + $hasGroup = filled($this->input('group_id')); + + if (! $hasClient && ! $hasGroup) { + $message = 'Un client ou un groupe de clients est obligatoire.'; + + $validator->errors()->add('client_id', $message); + $validator->errors()->add('group_id', $message); + } + + if ($hasClient && $hasGroup) { + $message = 'Selectionnez soit un client, soit un groupe de clients.'; + + $validator->errors()->add('client_id', $message); + $validator->errors()->add('group_id', $message); + } + }); + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdatePractitionerDocumentRequest.php b/thanasoft-back/app/Http/Requests/UpdatePractitionerDocumentRequest.php new file mode 100644 index 0000000..6ee2815 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdatePractitionerDocumentRequest.php @@ -0,0 +1,55 @@ + + */ + public function rules(): array + { + return [ + 'practitioner_id' => 'required|exists:thanatopractitioners,id', + 'doc_type' => 'required|string|max:191', + 'file_id' => 'nullable|exists:files,id', + 'issue_date' => 'nullable|date', + 'expiry_date' => 'nullable|date|after_or_equal:issue_date', + 'status' => 'nullable|string|max:64', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'practitioner_id.required' => 'Le thanatopractitioner est obligatoire.', + 'practitioner_id.exists' => 'Le thanatopractitioner sélectionné n\'existe pas.', + 'doc_type.required' => 'Le type de document est obligatoire.', + 'doc_type.string' => 'Le type de document doit être une chaîne de caractères.', + 'doc_type.max' => 'Le type de document ne peut pas dépasser :max caractères.', + 'file_id.exists' => 'Le fichier sélectionné n\'existe pas.', + 'issue_date.date' => 'La date de délivrance doit être une date valide.', + 'expiry_date.date' => 'La date d\'expiration doit être une date valide.', + 'expiry_date.after_or_equal' => 'La date d\'expiration doit être égale ou postérieure à la date de délivrance.', + 'status.string' => 'Le statut doit être une chaîne de caractères.', + 'status.max' => 'Le statut ne peut pas dépasser :max caractères.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdatePriceListRequest.php b/thanasoft-back/app/Http/Requests/UpdatePriceListRequest.php new file mode 100644 index 0000000..fd86bcb --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdatePriceListRequest.php @@ -0,0 +1,54 @@ +|string> + */ + public function rules(): array + { + $routePriceList = $this->route('price_list'); + $priceListId = is_object($routePriceList) + ? $routePriceList->id + : ($routePriceList ?? $this->route('id')); + + return [ + 'name' => [ + 'required', + 'string', + 'max:191', + Rule::unique('price_lists', 'name')->ignore($priceListId), + ], + 'valid_from' => 'nullable|date', + 'valid_to' => 'nullable|date|after_or_equal:valid_from', + 'is_default' => 'nullable|boolean', + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Le nom de la liste de prix est obligatoire.', + 'name.unique' => 'Une liste de prix avec ce nom existe déjà.', + 'valid_from.date' => 'La date de début doit être une date valide.', + 'valid_to.date' => 'La date de fin doit être une date valide.', + 'valid_to.after_or_equal' => 'La date de fin doit être postérieure ou égale à la date de début.', + 'is_default.boolean' => 'Le statut par défaut doit être vrai ou faux.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateProductCategoryRequest.php b/thanasoft-back/app/Http/Requests/UpdateProductCategoryRequest.php new file mode 100644 index 0000000..11f6cf1 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateProductCategoryRequest.php @@ -0,0 +1,48 @@ +|string> + */ + public function rules(): array + { + return [ + 'code' => [ + 'required', + 'string', + 'max:64', + Rule::unique('product_categories')->ignore($this->route('product_category')), + ], + 'name' => 'required|string|max:191', + 'description' => 'nullable|string', + 'parent_id' => [ + 'nullable', + 'exists:product_categories,id', + // Prevent setting parent to itself + function ($attribute, $value, $fail) { + if ($this->route('product_category') && $value == $this->route('product_category')->id) { + $fail('The parent category cannot be the category itself.'); + } + }, + ], + 'intervention' => 'boolean', + 'active' => 'boolean', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateProductRequest.php b/thanasoft-back/app/Http/Requests/UpdateProductRequest.php new file mode 100644 index 0000000..14b27b0 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateProductRequest.php @@ -0,0 +1,91 @@ +|string> + */ + public function rules(): array + { + $productId = $this->route('id'); + + return [ + 'nom' => 'required|string|max:255', + 'reference' => "nullable", + 'categorie_id' => 'required|exists:product_categories,id', + 'fabricant' => 'nullable|string|max:191', + 'stock_actuel' => 'required|numeric|min:0', + 'stock_minimum' => 'required|numeric|min:0', + 'unite' => 'required|string|max:50', + 'prix_unitaire' => 'required|numeric|min:0', + 'date_expiration' => 'nullable|date|after:today', + 'numero_lot' => 'nullable|string|max:100', + 'conditionnement_nom' => 'nullable|string|max:191', + 'conditionnement_quantite' => 'nullable|numeric|min:0', + 'conditionnement_unite' => 'nullable|string|max:50', + 'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048', + 'remove_image' => 'nullable|boolean', + 'fiche_technique_url' => 'nullable|url|max:500', + 'fournisseur_id' => 'nullable|exists:fournisseurs,id', + ]; + } + + public function messages(): array + { + return [ + 'nom.required' => 'Le nom du produit est obligatoire.', + 'nom.string' => 'Le nom du produit doit être une chaîne de caractères.', + 'nom.max' => 'Le nom du produit ne peut pas dépasser 255 caractères.', + 'reference.required' => 'La référence du produit est obligatoire.', + 'reference.string' => 'La référence du produit doit être une chaîne de caractères.', + 'reference.max' => 'La référence du produit ne peut pas dépasser 100 caractères.', + 'reference.unique' => 'Cette référence de produit existe déjà.', + 'categorie_id.required' => 'La catégorie est obligatoire.', + 'categorie_id.exists' => 'La catégorie sélectionnée n\'existe pas.', + 'fabricant.string' => 'Le fabricant doit être une chaîne de caractères.', + 'fabricant.max' => 'Le fabricant ne peut pas dépasser 191 caractères.', + 'stock_actuel.required' => 'Le stock actuel est obligatoire.', + 'stock_actuel.numeric' => 'Le stock actuel doit être un nombre.', + 'stock_actuel.min' => 'Le stock actuel doit être supérieur ou égal à 0.', + 'stock_minimum.required' => 'Le stock minimum est obligatoire.', + 'stock_minimum.numeric' => 'Le stock minimum doit être un nombre.', + 'stock_minimum.min' => 'Le stock minimum doit être supérieur ou égal à 0.', + 'unite.required' => 'L\'unité est obligatoire.', + 'unite.string' => 'L\'unité doit être une chaîne de caractères.', + 'unite.max' => 'L\'unité ne peut pas dépasser 50 caractères.', + 'prix_unitaire.required' => 'Le prix unitaire est obligatoire.', + 'prix_unitaire.numeric' => 'Le prix unitaire doit être un nombre.', + 'prix_unitaire.min' => 'Le prix unitaire doit être supérieur ou égal à 0.', + 'date_expiration.date' => 'La date d\'expiration doit être une date valide.', + 'date_expiration.after' => 'La date d\'expiration doit être postérieure à aujourd\'hui.', + 'numero_lot.string' => 'Le numéro de lot doit être une chaîne de caractères.', + 'numero_lot.max' => 'Le numéro de lot ne peut pas dépasser 100 caractères.', + 'conditionnement_nom.string' => 'Le nom du conditionnement doit être une chaîne de caractères.', + 'conditionnement_nom.max' => 'Le nom du conditionnement ne peut pas dépasser 191 caractères.', + 'conditionnement_quantite.numeric' => 'La quantité du conditionnement doit être un nombre.', + 'conditionnement_quantite.min' => 'La quantité du conditionnement doit être supérieure ou égal à 0.', + 'conditionnement_unite.string' => 'L\'unité du conditionnement doit être une chaîne de caractères.', + 'conditionnement_unite.max' => 'L\'unité du conditionnement ne peut pas dépasser 50 caractères.', + 'image.image' => 'Le fichier doit être une image valide.', + 'image.mimes' => 'L\'image doit être de type: jpeg, png, jpg, gif ou svg.', + 'image.max' => 'L\'image ne peut pas dépasser 2MB.', + 'fiche_technique_url.url' => 'L\'URL de la fiche technique doit être une URL valide.', + 'fiche_technique_url.max' => 'L\'URL de la fiche technique ne peut pas dépasser 500 caractères.', + 'fournisseur_id.exists' => 'Le fournisseur sélectionné n\'existe pas.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdatePurchaseOrderRequest.php b/thanasoft-back/app/Http/Requests/UpdatePurchaseOrderRequest.php new file mode 100644 index 0000000..82576d2 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdatePurchaseOrderRequest.php @@ -0,0 +1,48 @@ +|string> + */ + public function rules(): array + { + return [ + 'fournisseur_id' => ['nullable', 'exists:fournisseurs,id'], + 'po_number' => ['nullable', 'string', 'max:191', 'unique:purchase_orders,po_number,' . $this->route('purchase_order')], + 'status' => ['nullable', 'in:brouillon,confirmee,livree,facturee,annulee'], + 'order_date' => ['nullable', 'date'], + 'expected_date' => ['nullable', 'date'], + 'currency' => ['nullable', 'string', 'size:3'], + 'total_ht' => ['nullable', 'numeric', 'min:0'], + 'total_tva' => ['nullable', 'numeric', 'min:0'], + 'total_ttc' => ['nullable', 'numeric', 'min:0'], + 'notes' => ['nullable', 'string'], + 'delivery_address' => ['nullable', 'string'], + 'lines' => ['nullable', 'array', 'min:1'], + 'lines.*.product_id' => ['nullable', 'exists:products,id'], + 'lines.*.description' => ['required', 'string'], + 'lines.*.quantity' => ['required', 'numeric', 'min:0.001'], + 'lines.*.unit_price' => ['required', 'numeric', 'min:0'], + 'lines.*.tva_rate' => ['required', 'numeric', 'min:0'], + 'lines.*.discount_pct' => ['nullable', 'numeric', 'min:0', 'max:100'], + 'lines.*.total_ht' => ['required', 'numeric', 'min:0'], + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateQuoteLineRequest.php b/thanasoft-back/app/Http/Requests/UpdateQuoteLineRequest.php new file mode 100644 index 0000000..7916e87 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateQuoteLineRequest.php @@ -0,0 +1,39 @@ +|string> + */ + public function rules(): array + { + return [ + 'quote_id' => 'sometimes|exists:quotes,id', + 'product_id' => 'nullable|exists:products,id', + 'packaging_id' => 'nullable|exists:product_packagings,id', + 'packages_qty' => 'nullable|numeric|min:0', + 'units_qty' => 'nullable|numeric|min:0', + 'description' => 'sometimes|string', + 'qty_base' => 'nullable|numeric|min:0', + 'unit_price' => 'sometimes|numeric|min:0', + 'unit_price_per_package' => 'nullable|numeric|min:0', + 'tva_rate_id' => 'nullable|exists:tva_rates,id', + 'discount_pct' => 'sometimes|numeric|min:0|max:100', + 'total_ht' => 'sometimes|numeric|min:0', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php b/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php new file mode 100644 index 0000000..6fdd10c --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php @@ -0,0 +1,61 @@ +|string> + */ + public function rules(): array + { + $quoteId = $this->route('quote'); + + return [ + 'client_id' => 'sometimes|exists:clients,id', + 'group_id' => 'nullable|exists:client_groups,id', + 'reference' => 'sometimes|string|max:191|unique:quotes,reference,' . $quoteId, + 'status' => 'sometimes|in:brouillon,envoye,accepte,refuse,expire,annule', + 'quote_date' => 'sometimes|date', + 'valid_until' => 'nullable|date|after_or_equal:quote_date', + 'currency' => 'sometimes|string|size:3', + 'total_ht' => 'sometimes|numeric|min:0', + 'total_tva' => 'sometimes|numeric|min:0', + 'total_ttc' => 'sometimes|numeric|min:0', + ]; + } + + public function messages(): array + { + return [ + 'client_id.exists' => 'Le client sélectionné est invalide.', + 'group_id.exists' => 'Le groupe sélectionné est invalide.', + 'reference.string' => 'Le numéro de devis doit être une chaîne de caractères.', + 'reference.max' => 'Le numéro de devis ne doit pas dépasser 191 caractères.', + 'reference.unique' => 'Ce numéro de devis existe déjà.', + 'status.in' => 'Le statut sélectionné est invalide.', + 'quote_date.date' => 'La date du devis n\'est pas valide.', + 'valid_until.date' => 'La date de validité n\'est pas valide.', + 'valid_until.after_or_equal' => 'La date de validité doit être postérieure ou égale à la date du devis.', + 'currency.size' => 'La devise doit comporter 3 caractères.', + 'total_ht.numeric' => 'Le total HT doit être un nombre.', + 'total_ht.min' => 'Le total HT ne peut pas être négatif.', + 'total_tva.numeric' => 'Le total TVA doit être un nombre.', + 'total_tva.min' => 'Le total TVA ne peut pas être négatif.', + 'total_ttc.numeric' => 'Le total TTC doit être un nombre.', + 'total_ttc.min' => 'Le total TTC ne peut pas être négatif.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateStockItemRequest.php b/thanasoft-back/app/Http/Requests/UpdateStockItemRequest.php new file mode 100644 index 0000000..c6a2466 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateStockItemRequest.php @@ -0,0 +1,42 @@ +|string> + */ + public function rules(): array + { + return [ + 'qty_on_hand_base' => 'sometimes|numeric|min:0', + 'safety_stock_base' => 'sometimes|numeric|min:0', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'qty_on_hand_base.numeric' => 'La quantité en stock doit être un nombre.', + 'safety_stock_base.numeric' => 'Le stock de sécurité doit être un nombre.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateThanatopractitionerRequest.php b/thanasoft-back/app/Http/Requests/UpdateThanatopractitionerRequest.php new file mode 100644 index 0000000..74ff3c8 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateThanatopractitionerRequest.php @@ -0,0 +1,62 @@ + + */ + public function rules(): array + { + return [ + 'employee_id' => [ + 'nullable', + 'exists:employees,id', + Rule::unique('thanatopractitioners', 'employee_id')->ignore($this->route('thanatopractitioner')) + ], + 'diploma_number' => 'nullable|string|max:191', + 'diploma_date' => 'nullable|date', + 'authorization_number' => 'nullable|string|max:191', + 'authorization_issue_date' => 'nullable|date', + 'authorization_expiry_date' => 'nullable|date|after_or_equal:authorization_issue_date', + 'notes' => 'nullable|string', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'employee_id.required' => 'L\'employé est obligatoire.', + 'employee_id.exists' => 'L\'employé sélectionné n\'existe pas.', + 'employee_id.unique' => 'Cet employé est déjà enregistré comme thanatopractitioner.', + 'diploma_number.string' => 'Le numéro de diplôme doit être une chaîne de caractères.', + 'diploma_number.max' => 'Le numéro de diplôme ne peut pas dépasser :max caractères.', + 'diploma_date.date' => 'La date d\'obtention du diplôme doit être une date valide.', + 'authorization_number.string' => 'Le numéro d\'autorisation doit être une chaîne de caractères.', + 'authorization_number.max' => 'Le numéro d\'autorisation ne peut pas dépasser :max caractères.', + 'authorization_issue_date.date' => 'La date de délivrance de l\'autorisation doit être une date valide.', + 'authorization_expiry_date.date' => 'La date d\'expiration de l\'autorisation doit être une date valide.', + 'authorization_expiry_date.after_or_equal' => 'La date d\'expiration doit être égale ou postérieure à la date de délivrance.', + 'notes.string' => 'Les notes doivent être une chaîne de caractères.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateTvaRateRequest.php b/thanasoft-back/app/Http/Requests/UpdateTvaRateRequest.php new file mode 100644 index 0000000..7403c57 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateTvaRateRequest.php @@ -0,0 +1,45 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'sometimes|string|max:50', + 'rate' => 'sometimes|numeric|min:0|max:999.99', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'name.string' => 'Le nom doit être une chaîne de caractères.', + 'name.max' => 'Le nom ne peut pas dépasser 50 caractères.', + 'rate.numeric' => 'Le taux doit être un nombre.', + 'rate.min' => 'Le taux ne peut pas être négatif.', + 'rate.max' => 'Le taux ne peut pas dépasser 999.99.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateUserRequest.php b/thanasoft-back/app/Http/Requests/UpdateUserRequest.php new file mode 100644 index 0000000..8a5ce36 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateUserRequest.php @@ -0,0 +1,40 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique('users', 'email')->ignore($this->route('user')), + ], + 'roles' => ['nullable', 'array'], + 'roles.*' => ['string', 'max:100'], + 'permissions' => ['nullable', 'array'], + 'permissions.*' => ['string', 'max:150'], + 'password' => ['nullable', 'string', Password::min(8)], + 'clear_password' => ['nullable', 'boolean'], + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateVehicleRequest.php b/thanasoft-back/app/Http/Requests/UpdateVehicleRequest.php new file mode 100644 index 0000000..44a1fd3 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateVehicleRequest.php @@ -0,0 +1,35 @@ +route('vehicle') ?? $this->route('id'); + + return [ + 'photo_file_name' => ['nullable', 'string', 'max:255'], + 'photo_file_url' => ['nullable', 'string', 'max:2048'], + 'photo_mime_type' => ['nullable', 'string', 'max:100'], + 'photo_size' => ['nullable', 'integer', 'min:0'], + 'brand' => ['sometimes', 'required', 'string', 'max:255'], + 'model' => ['sometimes', 'required', 'string', 'max:255'], + 'registration_number' => ['sometimes', 'required', 'string', 'max:255', Rule::unique('vehicles', 'registration_number')->ignore($vehicleId)], + 'vehicle_type' => ['nullable', Rule::in(['hearse', 'transport_vehicle', 'utility', 'sedan'])], + 'fuel_type' => ['nullable', Rule::in(['diesel', 'petrol', 'electric', 'hybrid'])], + 'year' => ['nullable', 'integer', 'min:1900', 'max:2100'], + 'primary_user_id' => ['nullable', 'exists:employees,id'], + 'status' => ['nullable', Rule::in(['active', 'maintenance', 'out_of_service'])], + 'notes' => ['nullable', 'string'], + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateWarehouseRequest.php b/thanasoft-back/app/Http/Requests/UpdateWarehouseRequest.php new file mode 100644 index 0000000..153d147 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateWarehouseRequest.php @@ -0,0 +1,52 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'sometimes|required|string|max:191', + 'address_line1' => 'nullable|string|max:255', + 'address_line2' => 'nullable|string|max:255', + 'postal_code' => 'nullable|string|max:20', + 'city' => 'nullable|string|max:191', + 'country_code' => 'nullable|string|size:2', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'name.required' => 'Le nom de l\'entrepôt est requis.', + 'name.string' => 'Le nom doit être une chaîne de caractères.', + 'name.max' => 'Le nom ne peut pas dépasser 191 caractères.', + 'address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.', + 'address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.', + 'postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.', + 'city.max' => 'La ville ne peut pas dépasser 191 caractères.', + 'country_code.size' => 'Le code pays doit comporter exactement 2 caractères.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateWebmailMessageRequest.php b/thanasoft-back/app/Http/Requests/UpdateWebmailMessageRequest.php new file mode 100644 index 0000000..a1321a4 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateWebmailMessageRequest.php @@ -0,0 +1,31 @@ + + */ + public function rules(): array + { + return [ + 'folder' => ['nullable', 'string', 'max:30'], + 'status' => ['nullable', 'string', 'max:30'], + 'is_read' => ['nullable', 'boolean'], + 'is_starred' => ['nullable', 'boolean'], + 'subject' => ['nullable', 'string', 'max:255'], + 'body' => ['nullable', 'string'], + 'metadata' => ['nullable', 'array'], + ]; + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Http/Requests/UpsertUserMailboxSettingRequest.php b/thanasoft-back/app/Http/Requests/UpsertUserMailboxSettingRequest.php new file mode 100644 index 0000000..2f39341 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpsertUserMailboxSettingRequest.php @@ -0,0 +1,41 @@ + + */ + public function rules(): array + { + return [ + 'imap_host' => ['nullable', 'string', 'max:255'], + 'imap_port' => ['nullable', 'integer', 'min:1', 'max:65535'], + 'imap_encryption' => ['nullable', 'string', 'in:ssl,tls,starttls,none'], + 'imap_validate_cert' => ['nullable', 'boolean'], + 'imap_username' => ['nullable', 'string', 'max:255'], + 'imap_password' => ['nullable', 'string', 'max:500'], + 'imap_folder' => ['nullable', 'string', 'max:255'], + 'smtp_host' => ['nullable', 'string', 'max:255'], + 'smtp_port' => ['nullable', 'integer', 'min:1', 'max:65535'], + 'smtp_encryption' => ['nullable', 'string', 'in:ssl,tls,none'], + 'smtp_validate_cert' => ['nullable', 'boolean'], + 'smtp_username' => ['nullable', 'string', 'max:255'], + 'smtp_password' => ['nullable', 'string', 'max:500'], + 'smtp_from_address' => ['nullable', 'email:rfc,dns'], + 'smtp_from_name' => ['nullable', 'string', 'max:255'], + 'clear_imap_password' => ['nullable', 'boolean'], + 'clear_smtp_password' => ['nullable', 'boolean'], + ]; + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Http/Resources/AvoirLineResource.php b/thanasoft-back/app/Http/Resources/AvoirLineResource.php new file mode 100644 index 0000000..1f53d95 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/AvoirLineResource.php @@ -0,0 +1,28 @@ + $this->id, + 'avoir_id' => $this->avoir_id, + 'product_id' => $this->product_id, + 'invoice_line_id' => $this->invoice_line_id, + 'description' => $this->description, + 'quantity' => $this->quantity, + 'unit_price' => $this->unit_price, + 'tva_rate' => $this->tva_rate, + 'total_ht' => $this->total_ht, + 'total_tva' => $this->total_tva, + 'total_ttc' => $this->total_ttc, + 'notes' => $this->notes, + 'product' => $this->whenLoaded('product'), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/AvoirResource.php b/thanasoft-back/app/Http/Resources/AvoirResource.php new file mode 100644 index 0000000..f0e54e2 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/AvoirResource.php @@ -0,0 +1,42 @@ + $this->id, + 'client_id' => $this->client_id, + 'invoice_id' => $this->invoice_id, + 'group_id' => $this->group_id, + 'avoir_number' => $this->avoir_number, + 'status' => $this->status, + 'avoir_date' => $this->avoir_date, + 'due_date' => $this->due_date, + 'currency' => $this->currency, + 'total_ht' => $this->total_ht, + 'total_tva' => $this->total_tva, + 'total_ttc' => $this->total_ttc, + 'reason_type' => $this->reason_type, + 'reason_description' => $this->reason_description, + 'e_invoice_status' => $this->e_invoice_status, + 'refund_status' => $this->refund_status, + 'refund_date' => $this->refund_date, + 'refund_method' => $this->refund_method, + 'compensation_invoice_id' => $this->compensation_invoice_id, + 'compensation_amount' => $this->compensation_amount, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + 'client' => $this->whenLoaded('client'), + 'invoice' => $this->whenLoaded('invoice'), + 'group' => $this->whenLoaded('group'), + 'lines' => AvoirLineResource::collection($this->whenLoaded('lines')), + 'history' => DocumentStatusHistoryResource::collection($this->whenLoaded('history')), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Client/ClientActivityTimelineResource.php b/thanasoft-back/app/Http/Resources/Client/ClientActivityTimelineResource.php new file mode 100644 index 0000000..3698483 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Client/ClientActivityTimelineResource.php @@ -0,0 +1,66 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'client_id' => $this->client_id, + 'actor_type' => $this->actor_type, + 'actor_name' => $this->actor ? $this->actor->firstname . ' ' . $this->actor->lastname : 'System', + 'event_type' => $this->event_type, + 'entity_type' => $this->entity_type, + 'entity_id' => $this->entity_id, + 'title' => $this->title, + 'description' => $this->description, + 'metadata' => $this->metadata, + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'time_ago' => $this->created_at?->diffForHumans(), + + // Helper properties for frontend display + 'icon' => $this->getIcon(), + 'color' => $this->getColor(), + ]; + } + + protected function getIcon() + { + // Map event types to icons (using Nucleo icons as requested) + return match($this->event_type) { + 'call' => 'mobile-button', + 'email_sent', 'email_received' => 'email-83', + 'invoice_created', 'invoice_sent', 'invoice_paid' => 'money-coins', + 'quote_created', 'quote_sent' => 'single-copy-04', + 'file_uploaded', 'attachment_sent', 'attachment_received' => 'cloud-upload-96', + 'client_created' => 'badge-24', + 'meeting', 'intervention_created' => 'laptop', + default => 'bell-55' + }; + } + + protected function getColor() + { + // Map event types to colors + return match($this->event_type) { + 'client_created' => 'success', + 'call' => 'info', + 'email_sent' => 'success', + 'invoice_paid' => 'success', + 'invoice_created' => 'warning', + 'quote_created' => 'primary', + 'quote_sent' => 'primary', + default => 'dark' + }; + } +} diff --git a/thanasoft-back/app/Http/Resources/Client/ClientCategoryResource.php b/thanasoft-back/app/Http/Resources/Client/ClientCategoryResource.php new file mode 100644 index 0000000..62e4cbe --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Client/ClientCategoryResource.php @@ -0,0 +1,32 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'slug' => $this->slug, + 'description' => $this->description, + 'is_active' => $this->is_active, + 'sort_order' => $this->sort_order, + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + + // Relationships (loaded when needed) + // 'clients_count' => $this->whenCounted('clients'), + // 'clients' => ClientResource::collection($this->whenLoaded('clients')), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Client/ClientCollection.php b/thanasoft-back/app/Http/Resources/Client/ClientCollection.php new file mode 100644 index 0000000..c95bbc5 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Client/ClientCollection.php @@ -0,0 +1,48 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'data' => $this->collection, + 'meta' => [ + 'total' => $this->total(), + 'per_page' => $this->perPage(), + 'current_page' => $this->currentPage(), + 'last_page' => $this->lastPage(), + 'from' => $this->firstItem(), + 'to' => $this->lastItem(), + 'stats' => [ + 'active' => $this->collection->where('is_active', true)->count(), + 'inactive' => $this->collection->where('is_active', false)->count(), + 'by_type' => $this->collection->groupBy('client_category_id')->map->count(), + ], + ], + 'links' => [ + 'first' => $this->url(1), + 'last' => $this->url($this->lastPage()), + 'prev' => $this->previousPageUrl(), + 'next' => $this->nextPageUrl(), + ], + ]; + } + + public function with(Request $request): array + { + return [ + 'status' => 'success', + 'message' => 'Clients récupérés avec succès', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Client/ClientGroupCollection.php b/thanasoft-back/app/Http/Resources/Client/ClientGroupCollection.php new file mode 100644 index 0000000..2850e77 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Client/ClientGroupCollection.php @@ -0,0 +1,43 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'data' => $this->collection, + 'meta' => [ + 'total' => $this->total(), + 'per_page' => $this->perPage(), + 'current_page' => $this->currentPage(), + 'last_page' => $this->lastPage(), + 'from' => $this->firstItem(), + 'to' => $this->lastItem(), + ], + 'links' => [ + 'first' => $this->url(1), + 'last' => $this->url($this->lastPage()), + 'prev' => $this->previousPageUrl(), + 'next' => $this->nextPageUrl(), + ], + ]; + } + + public function with(Request $request): array + { + return [ + 'status' => 'success', + 'message' => 'Groupes de clients récupérés avec succès', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Client/ClientGroupResource.php b/thanasoft-back/app/Http/Resources/Client/ClientGroupResource.php new file mode 100644 index 0000000..7b85a69 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Client/ClientGroupResource.php @@ -0,0 +1,29 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'description' => $this->description ?? null, + 'clients_count' => $this->whenCounted('clients'), + 'client_ids' => $this->whenLoaded('clients', fn () => $this->clients->pluck('id')->values()), + 'clients' => ClientResource::collection($this->whenLoaded('clients')), + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Client/ClientLocationCollection.php b/thanasoft-back/app/Http/Resources/Client/ClientLocationCollection.php new file mode 100644 index 0000000..aa6fdd3 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Client/ClientLocationCollection.php @@ -0,0 +1,47 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'data' => $this->collection, + 'meta' => [ + 'total' => $this->total(), + 'per_page' => $this->perPage(), + 'current_page' => $this->currentPage(), + 'last_page' => $this->lastPage(), + 'from' => $this->firstItem(), + 'to' => $this->lastItem(), + 'stats' => [ + 'default_locations' => $this->collection->where('is_default', true)->count(), + 'with_gps' => $this->collection->filter(fn($location) => $location->gps_lat && $location->gps_lng)->count(), + ], + ], + 'links' => [ + 'first' => $this->url(1), + 'last' => $this->url($this->lastPage()), + 'prev' => $this->previousPageUrl(), + 'next' => $this->nextPageUrl(), + ], + ]; + } + + public function with(Request $request): array + { + return [ + 'status' => 'success', + 'message' => 'Lieux clients récupérés avec succès', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Client/ClientLocationResource.php b/thanasoft-back/app/Http/Resources/Client/ClientLocationResource.php new file mode 100644 index 0000000..3e9fb20 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Client/ClientLocationResource.php @@ -0,0 +1,51 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'client_id' => $this->client_id, + 'name' => $this->name, + 'address' => [ + 'line1' => $this->address_line1, + 'line2' => $this->address_line2, + 'postal_code' => $this->postal_code, + 'city' => $this->city, + 'country_code' => $this->country_code, + 'full_address' => $this->full_address, + ], + 'gps_coordinates' => $this->gps_coordinates, + 'gps_lat' => $this->gps_lat, + 'gps_lng' => $this->gps_lng, + 'is_default' => $this->is_default, + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + + // Relations + 'client' => new ClientResource($this->whenLoaded('client')), + //'interventions_as_origin' => InterventionResource::collection($this->whenLoaded('interventionsAsOrigin')), + //'transports_as_origin' => TransportResource::collection($this->whenLoaded('transportsAsOrigin')), + //'transports_as_destination' => TransportResource::collection($this->whenLoaded('transportsAsDestination')), + ]; + } + + public function with(Request $request): array + { + return [ + 'status' => 'success', + 'message' => 'Lieu client récupéré avec succès', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Client/ClientResource.php b/thanasoft-back/app/Http/Resources/Client/ClientResource.php new file mode 100644 index 0000000..cf46492 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Client/ClientResource.php @@ -0,0 +1,67 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + //'company_id' => $this->company_id, + 'commercial' => $this->commercial(), + 'type_label' => $this->getTypeLabel(), + 'name' => $this->name, + 'vat_number' => $this->vat_number, + 'siret' => $this->siret, + 'email' => $this->email, + 'phone' => $this->phone, + 'avatar_url' => $this->avatar ? Storage::url($this->avatar) : null, + 'billing_address' => [ + 'line1' => $this->billing_address_line1, + 'line2' => $this->billing_address_line2, + 'postal_code' => $this->billing_postal_code, + 'city' => $this->billing_city, + 'country_code' => $this->billing_country_code, + 'full_address' => $this->billing_address, + ], + 'group_id' => $this->group_id, + 'notes' => $this->notes, + 'is_active' => $this->is_active, + 'is_parent' => $this->is_parent, + 'parent_id' => $this->parent_id, + // 'default_tva_rate_id' => $this->default_tva_rate_id, + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + + // Counts + 'contacts_count' => $this->whenCounted('contacts'), + 'locations_count' => $this->whenCounted('locations'), + // 'interventions_count' => $this->whenCounted('interventions'), + // 'quotes_count' => $this->whenCounted('quotes'), + // 'invoices_count' => $this->whenCounted('invoices'), + + // Relations + // 'company' => new CompanyResource($this->whenLoaded('company')), + 'group' => new ClientGroupResource($this->whenLoaded('group')), + 'parent' => new ClientResource($this->whenLoaded('parent')), + // 'default_tva_rate' => new TvaRateResource($this->whenLoaded('defaultTvaRate')), + 'contacts' => ContactResource::collection($this->whenLoaded('contacts')), + 'locations' => ClientLocationResource::collection($this->whenLoaded('locations')), + // 'price_lists' => PriceListResource::collection($this->whenLoaded('priceLists')), + // 'interventions' => InterventionResource::collection($this->whenLoaded('interventions')), + // 'quotes' => QuoteResource::collection($this->whenLoaded('quotes')), + // 'invoices' => InvoiceResource::collection($this->whenLoaded('invoices')), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/ClientGroupResource.php b/thanasoft-back/app/Http/Resources/ClientGroupResource.php new file mode 100644 index 0000000..0eb97cc --- /dev/null +++ b/thanasoft-back/app/Http/Resources/ClientGroupResource.php @@ -0,0 +1,25 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'description' => $this->description, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Contact/ContactCollection.php b/thanasoft-back/app/Http/Resources/Contact/ContactCollection.php new file mode 100644 index 0000000..217c0ca --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Contact/ContactCollection.php @@ -0,0 +1,43 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'data' => ContactResource::collection($this->collection), + 'meta' => [ + 'total' => $this->total(), + 'per_page' => $this->perPage(), + 'current_page' => $this->currentPage(), + 'last_page' => $this->lastPage(), + 'from' => $this->firstItem(), + 'to' => $this->lastItem(), + ], + 'links' => [ + 'first' => $this->url(1), + 'last' => $this->url($this->lastPage()), + 'prev' => $this->previousPageUrl(), + 'next' => $this->nextPageUrl(), + ], + ]; + } + + public function with(Request $request): array + { + return [ + 'status' => 'success', + 'message' => 'Contacts récupérés avec succès', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Contact/ContactResource.php b/thanasoft-back/app/Http/Resources/Contact/ContactResource.php new file mode 100644 index 0000000..a377c23 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Contact/ContactResource.php @@ -0,0 +1,54 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'client_id' => $this->client_id, + 'fournisseur_id' => $this->fournisseur_id, + 'first_name' => $this->first_name, + 'last_name' => $this->last_name, + 'full_name' => $this->full_name, + 'email' => $this->email, + 'phone' => $this->phone, + 'role' => $this->role, + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + + // Relations + 'client' => $this->whenLoaded('client', function() { + return $this->client ? [ + 'id' => $this->client->id, + 'name' => $this->client->name, + ] : null; + }), + 'fournisseur' => $this->whenLoaded('fournisseur', function() { + return $this->fournisseur ? [ + 'id' => $this->fournisseur->id, + 'name' => $this->fournisseur->name, + ] : null; + }), + ]; + } + + public function with(Request $request): array + { + return [ + 'status' => 'success', + 'message' => 'Contact récupéré avec succès', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Convoy/ConvoyResource.php b/thanasoft-back/app/Http/Resources/Convoy/ConvoyResource.php new file mode 100644 index 0000000..3f8bae5 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Convoy/ConvoyResource.php @@ -0,0 +1,79 @@ + $this->id, + 'deceased_id' => $this->deceased_id, + 'client_id' => $this->client_id, + 'vehicle_id' => $this->vehicle_id, + 'mission_title' => $this->mission_title, + 'convoy_type' => $this->convoy_type, + 'transport_mode' => $this->transport_mode, + 'status' => $this->status, + 'planned_start_at' => $this->planned_start_at?->format('Y-m-d H:i:s'), + 'estimated_end_at' => $this->estimated_end_at?->format('Y-m-d H:i:s'), + 'family_email' => $this->family_email, + 'automatic_notifications' => $this->automatic_notifications, + 'departure' => [ + 'location_selection_mode' => $this->departure_location_selection_mode, + 'location_id' => $this->departure_location_id, + 'location' => $this->whenLoaded('departureLocation', function () { + return $this->departureLocation ? [ + 'id' => $this->departureLocation->id, + 'client_id' => $this->departureLocation->client_id, + 'name' => $this->departureLocation->name, + 'address_line1' => $this->departureLocation->address_line1, + 'address_line2' => $this->departureLocation->address_line2, + 'postal_code' => $this->departureLocation->postal_code, + 'city' => $this->departureLocation->city, + 'country_code' => $this->departureLocation->country_code, + 'gps_lat' => $this->departureLocation->gps_lat, + 'gps_lng' => $this->departureLocation->gps_lng, + ] : null; + }), + 'name' => $this->departure_name, + 'address' => $this->departure_address, + 'city' => $this->departure_city, + 'postal_code' => $this->departure_postal_code, + 'country_code' => $this->departure_country_code, + 'latitude' => $this->departure_latitude, + 'longitude' => $this->departure_longitude, + 'additional_details' => $this->departure_additional_details, + ], + 'tabs' => [ + 'itinerary' => $this->tab_itinerary, + 'legal_documents' => $this->tab_legal_documents, + 'team_resources' => $this->tab_team_resources, + 'procedures' => $this->tab_procedures, + 'cost_tracking' => $this->tab_cost_tracking, + 'fuel' => $this->tab_fuel, + 'ceremony' => $this->tab_ceremony, + 'thanatopraxy' => $this->tab_thanatopraxy, + 'gps_tracking_steps' => $this->tab_gps_tracking_steps, + 'communication' => $this->tab_communication, + ], + 'deceased' => $this->whenLoaded('deceased', function () { + return new DeceasedResource($this->deceased); + }), + 'client' => $this->whenLoaded('client', function () { + return $this->client ? new ClientResource($this->client) : null; + }), + 'vehicle' => $this->whenLoaded('vehicle', function () { + return $this->vehicle ? new VehicleResource($this->vehicle) : null; + }), + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Deceased/DeceasedCollection.php b/thanasoft-back/app/Http/Resources/Deceased/DeceasedCollection.php new file mode 100644 index 0000000..9ea0fdf --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Deceased/DeceasedCollection.php @@ -0,0 +1,29 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'data' => $this->collection, + 'meta' => [ + 'total' => $this->total(), + 'per_page' => $this->perPage(), + 'current_page' => $this->currentPage(), + 'last_page' => $this->lastPage(), + 'from' => $this->firstItem(), + 'to' => $this->lastItem() + ] + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Deceased/DeceasedDocumentCollection.php b/thanasoft-back/app/Http/Resources/Deceased/DeceasedDocumentCollection.php new file mode 100644 index 0000000..c0b76a7 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Deceased/DeceasedDocumentCollection.php @@ -0,0 +1,48 @@ + + */ + public function toArray($request): array + { + return [ + 'data' => $this->collection->map(function ($document) { + return [ + 'id' => $document->id, + 'deceased_id' => $document->deceased_id, + 'doc_type' => $document->doc_type, + 'file_id' => $document->file_id, + 'generated_at' => $document->generated_at?->format('Y-m-d H:i:s'), + 'created_at' => $document->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $document->updated_at?->format('Y-m-d H:i:s'), + + // Relations + 'deceased' => $document->deceased ? [ + 'id' => $document->deceased->id, + 'first_name' => $document->deceased->first_name, + 'last_name' => $document->deceased->last_name, + 'full_name' => $document->deceased->first_name . ' ' . $document->deceased->last_name, + 'date_of_birth' => $document->deceased->date_of_birth?->format('Y-m-d'), + 'date_of_death' => $document->deceased->date_of_death?->format('Y-m-d'), + ] : null, + 'file' => $document->file ? [ + 'id' => $document->file->id, + 'filename' => $document->file->filename ?? null, + 'path' => $document->file->path ?? null, + 'mime_type' => $document->file->mime_type ?? null, + 'size' => $document->file->size ?? null, + ] : null, + ]; + }), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Deceased/DeceasedDocumentResource.php b/thanasoft-back/app/Http/Resources/Deceased/DeceasedDocumentResource.php new file mode 100644 index 0000000..633d636 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Deceased/DeceasedDocumentResource.php @@ -0,0 +1,54 @@ + + */ + public function toArray($request): array + { + return [ + 'id' => $this->id, + 'deceased_id' => $this->deceased_id, + 'doc_type' => $this->doc_type, + 'file_id' => $this->file_id, + 'generated_at' => $this->generated_at?->format('Y-m-d H:i:s'), + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + + // Relations + 'deceased' => $this->when( + $this->relationLoaded('deceased'), + function () { + return [ + 'id' => $this->deceased->id, + 'first_name' => $this->deceased->first_name, + 'last_name' => $this->deceased->last_name, + 'full_name' => $this->deceased->first_name . ' ' . $this->deceased->last_name, + 'date_of_birth' => $this->deceased->date_of_birth?->format('Y-m-d'), + 'date_of_death' => $this->deceased->date_of_death?->format('Y-m-d'), + ]; + } + ), + 'file' => $this->when( + $this->relationLoaded('file'), + function () { + return $this->file ? [ + 'id' => $this->file->id, + 'filename' => $this->file->filename ?? null, + 'path' => $this->file->path ?? null, + 'mime_type' => $this->file->mime_type ?? null, + 'size' => $this->file->size ?? null, + ] : null; + } + ), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Deceased/DeceasedResource.php b/thanasoft-back/app/Http/Resources/Deceased/DeceasedResource.php new file mode 100644 index 0000000..b4db6b7 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Deceased/DeceasedResource.php @@ -0,0 +1,32 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'last_name' => $this->last_name, + 'first_name' => $this->first_name, + 'full_name' => trim($this->first_name . ' ' . $this->last_name), + 'birth_date' => $this->birth_date ? $this->birth_date->format('Y-m-d') : null, + 'death_date' => $this->death_date ? $this->death_date->format('Y-m-d') : null, + 'place_of_death' => $this->place_of_death, + 'notes' => $this->notes, + 'documents_count' => $this->documents_count ?? $this->documents()->count(), + 'interventions_count' => $this->interventions_count ?? $this->interventions()->count(), + 'created_at' => $this->created_at->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at->format('Y-m-d H:i:s') + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/DocumentStatusHistoryResource.php b/thanasoft-back/app/Http/Resources/DocumentStatusHistoryResource.php new file mode 100644 index 0000000..d7a8d35 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/DocumentStatusHistoryResource.php @@ -0,0 +1,26 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'old_status' => $this->old_status, + 'new_status' => $this->new_status, + 'changed_at' => $this->changed_at, + 'changed_by' => $this->user ? $this->user->name : 'System', // Simple user display + 'comment' => $this->comment, + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Employee/EmployeeCollection.php b/thanasoft-back/app/Http/Resources/Employee/EmployeeCollection.php new file mode 100644 index 0000000..0c54e46 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Employee/EmployeeCollection.php @@ -0,0 +1,43 @@ + + */ + public function toArray($request): array + { + return [ + 'data' => $this->collection->map(function ($employee) { + return [ + 'id' => $employee->id, + 'first_name' => $employee->first_name, + 'last_name' => $employee->last_name, + 'full_name' => $employee->full_name, + 'email' => $employee->email, + 'phone' => $employee->phone, + 'job_title' => $employee->job_title, + 'hire_date' => $employee->hire_date?->format('Y-m-d'), + 'active' => $employee->active, + 'created_at' => $employee->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $employee->updated_at?->format('Y-m-d H:i:s'), + + // Relations + 'thanatopractitioner' => $employee->thanatopractitioner ? [ + 'id' => $employee->thanatopractitioner->id, + 'diploma_number' => $employee->thanatopractitioner->diploma_number, + 'authorization_number' => $employee->thanatopractitioner->authorization_number, + 'is_authorization_valid' => $employee->thanatopractitioner->is_authorization_valid, + ] : null, + ]; + }), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Employee/EmployeeResource.php b/thanasoft-back/app/Http/Resources/Employee/EmployeeResource.php new file mode 100644 index 0000000..2f681d9 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Employee/EmployeeResource.php @@ -0,0 +1,47 @@ + + */ + public function toArray($request): array + { + return [ + 'id' => $this->id, + 'first_name' => $this->first_name, + 'last_name' => $this->last_name, + 'full_name' => $this->full_name, + 'email' => $this->email, + 'phone' => $this->phone, + 'user_id' => $this->user_id, + 'job_title' => $this->job_title, + 'hire_date' => $this->hire_date?->format('Y-m-d'), + 'active' => $this->active, + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + + // Relations + 'thanatopractitioner' => $this->when( + $this->relationLoaded('thanatopractitioner'), + new ThanatopractitionerResource($this->thanatopractitioner) + ), + 'user' => $this->when( + $this->relationLoaded('user') && $this->user, + fn () => [ + 'id' => $this->user->id, + 'name' => $this->user->name, + 'email' => $this->user->email, + 'employee_id' => $this->id, + ] + ), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Employee/PractitionerDocumentCollection.php b/thanasoft-back/app/Http/Resources/Employee/PractitionerDocumentCollection.php new file mode 100644 index 0000000..9cc2780 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Employee/PractitionerDocumentCollection.php @@ -0,0 +1,48 @@ + + */ + public function toArray($request): array + { + return [ + 'data' => $this->collection->map(function ($document) { + return [ + 'id' => $document->id, + 'practitioner_id' => $document->practitioner_id, + 'doc_type' => $document->doc_type, + 'file_id' => $document->file_id, + 'issue_date' => $document->issue_date?->format('Y-m-d'), + 'expiry_date' => $document->expiry_date?->format('Y-m-d'), + 'status' => $document->status, + 'is_valid' => $document->is_valid, + 'created_at' => $document->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $document->updated_at?->format('Y-m-d H:i:s'), + + // Relations + 'thanatopractitioner' => $document->thanatopractitioner ? [ + 'id' => $document->thanatopractitioner->id, + 'employee_id' => $document->thanatopractitioner->employee_id, + 'diploma_number' => $document->thanatopractitioner->diploma_number, + 'authorization_number' => $document->thanatopractitioner->authorization_number, + 'employee' => $document->thanatopractitioner->employee ? [ + 'id' => $document->thanatopractitioner->employee->id, + 'first_name' => $document->thanatopractitioner->employee->first_name, + 'last_name' => $document->thanatopractitioner->employee->last_name, + 'full_name' => $document->thanatopractitioner->employee->full_name, + ] : null, + ] : null, + ]; + }), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Employee/PractitionerDocumentResource.php b/thanasoft-back/app/Http/Resources/Employee/PractitionerDocumentResource.php new file mode 100644 index 0000000..dd467d2 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Employee/PractitionerDocumentResource.php @@ -0,0 +1,36 @@ + + */ + public function toArray($request): array + { + return [ + 'id' => $this->id, + 'practitioner_id' => $this->practitioner_id, + 'doc_type' => $this->doc_type, + 'file_id' => $this->file_id, + 'issue_date' => $this->issue_date?->format('Y-m-d'), + 'expiry_date' => $this->expiry_date?->format('Y-m-d'), + 'status' => $this->status, + 'is_valid' => $this->is_valid, + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + + // Relations + 'thanatopractitioner' => $this->when( + $this->relationLoaded('thanatopractitioner'), + new ThanatopractitionerResource($this->thanatopractitioner) + ), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Employee/ThanatopractitionerCollection.php b/thanasoft-back/app/Http/Resources/Employee/ThanatopractitionerCollection.php new file mode 100644 index 0000000..d86aad5 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Employee/ThanatopractitionerCollection.php @@ -0,0 +1,45 @@ + + */ + public function toArray($request): array + { + return [ + 'data' => $this->collection->map(function ($thanatopractitioner) { + return [ + 'id' => $thanatopractitioner->id, + 'employee_id' => $thanatopractitioner->employee_id, + 'diploma_number' => $thanatopractitioner->diploma_number, + 'diploma_date' => $thanatopractitioner->diploma_date?->format('Y-m-d'), + 'authorization_number' => $thanatopractitioner->authorization_number, + 'authorization_issue_date' => $thanatopractitioner->authorization_issue_date?->format('Y-m-d'), + 'authorization_expiry_date' => $thanatopractitioner->authorization_expiry_date?->format('Y-m-d'), + 'notes' => $thanatopractitioner->notes, + 'is_authorization_valid' => $thanatopractitioner->is_authorization_valid, + 'created_at' => $thanatopractitioner->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $thanatopractitioner->updated_at?->format('Y-m-d H:i:s'), + + // Relations + 'employee' => $thanatopractitioner->employee ? [ + 'id' => $thanatopractitioner->employee->id, + 'first_name' => $thanatopractitioner->employee->first_name, + 'last_name' => $thanatopractitioner->employee->last_name, + 'full_name' => $thanatopractitioner->employee->full_name, + 'email' => $thanatopractitioner->employee->email, + 'job_title' => $thanatopractitioner->employee->job_title, + ] : null, + ]; + }), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Employee/ThanatopractitionerResource.php b/thanasoft-back/app/Http/Resources/Employee/ThanatopractitionerResource.php new file mode 100644 index 0000000..8c131f1 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Employee/ThanatopractitionerResource.php @@ -0,0 +1,47 @@ + + */ + public function toArray($request): array + { + return [ + 'id' => $this->id, + 'employee_id' => $this->employee_id, + 'employee_name' => $this->when( + $this->relationLoaded('employee'), + $this->employee->full_name ?? ($this->employee->first_name . ' ' . $this->employee->last_name) + ), + 'diploma_number' => $this->diploma_number, + 'diploma_date' => $this->diploma_date?->format('Y-m-d'), + 'authorization_number' => $this->authorization_number, + 'authorization_issue_date' => $this->authorization_issue_date?->format('Y-m-d'), + 'authorization_expiry_date' => $this->authorization_expiry_date?->format('Y-m-d'), + 'notes' => $this->notes, + 'is_authorization_valid' => $this->is_authorization_valid, + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + + // Relations + 'employee' => $this->when( + $this->relationLoaded('employee'), + new EmployeeResource($this->employee) + ), + 'documents' => $this->when( + $this->relationLoaded('documents'), + PractitionerDocumentResource::collection($this->documents) + ), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/File/FileCollection.php b/thanasoft-back/app/Http/Resources/File/FileCollection.php new file mode 100644 index 0000000..847babd --- /dev/null +++ b/thanasoft-back/app/Http/Resources/File/FileCollection.php @@ -0,0 +1,86 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'data' => FileResource::collection($this->collection), + 'pagination' => [ + 'current_page' => $this->currentPage(), + 'from' => $this->firstItem(), + 'last_page' => $this->lastPage(), + 'per_page' => $this->perPage(), + 'to' => $this->lastItem(), + 'total' => $this->total(), + 'has_more_pages' => $this->hasMorePages(), + ], + 'summary' => [ + 'total_files' => $this->collection->count(), + 'total_size' => $this->collection->sum('size_bytes'), + 'total_size_formatted' => $this->formatBytes($this->collection->sum('size_bytes')), + 'categories' => $this->getCategoryStats(), + ], + ]; + } + + /** + * Calculate category statistics from the collection + */ + private function getCategoryStats(): array + { + $categories = []; + + foreach ($this->collection as $file) { + $pathParts = explode('/', $file->storage_uri); + $category = $pathParts[count($pathParts) - 3] ?? 'general'; + + if (!isset($categories[$category])) { + $categories[$category] = [ + 'count' => 0, + 'total_size' => 0, + 'files' => [] + ]; + } + + $categories[$category]['count']++; + $categories[$category]['total_size'] += $file->size_bytes ?? 0; + $categories[$category]['files'][] = $file->file_name; + } + + // Format sizes + foreach ($categories as $category => &$stats) { + $stats['total_size_formatted'] = $this->formatBytes($stats['total_size']); + // Remove file list to avoid too much data in collection + unset($stats['files']); + } + + return $categories; + } + + /** + * Format bytes to human readable format + */ + private function formatBytes(int $bytes, int $precision = 2): string + { + if ($bytes === 0) { + return '0 B'; + } + + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + $base = 1024; + $factor = floor((strlen($bytes) - 1) / 3); + + return sprintf("%.{$precision}f", $bytes / pow($base, $factor)) . ' ' . $units[$factor]; + } +} diff --git a/thanasoft-back/app/Http/Resources/File/FileResource.php b/thanasoft-back/app/Http/Resources/File/FileResource.php new file mode 100644 index 0000000..18d17af --- /dev/null +++ b/thanasoft-back/app/Http/Resources/File/FileResource.php @@ -0,0 +1,68 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'file_name' => $this->file_name, + 'mime_type' => $this->mime_type, + 'size_bytes' => $this->size_bytes, + 'size_formatted' => $this->formatted_size, + 'extension' => $this->extension, + 'storage_uri' => $this->storage_uri, + 'organized_path' => $this->organized_path, + 'sha256' => $this->sha256, + 'uploaded_by' => $this->uploaded_by, + 'uploader_name' => $this->uploader_name, + 'uploaded_at' => $this->uploaded_at?->format('Y-m-d H:i:s'), + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + + // File type helpers + 'is_image' => $this->is_image, + 'is_pdf' => $this->is_pdf, + + // URL for accessing the file (if public) + 'url' => $this->when( + $this->is_public ?? false, + asset('storage/' . $this->storage_uri) + ), + + // Relations + 'user' => [ + 'id' => $this->user?->id, + 'name' => $this->user?->name, + 'email' => $this->user?->email, + ], + + // Additional metadata from the file's path structure + 'category' => $this->when( + $this->storage_uri, + function () { + $pathParts = explode('/', $this->storage_uri); + return $pathParts[count($pathParts) - 3] ?? 'general'; + } + ), + + 'subcategory' => $this->when( + $this->storage_uri, + function () { + $pathParts = explode('/', $this->storage_uri); + return $pathParts[count($pathParts) - 2] ?? 'general'; + } + ), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/FileAttachment/FileAttachmentResource.php b/thanasoft-back/app/Http/Resources/FileAttachment/FileAttachmentResource.php new file mode 100644 index 0000000..6693d8a --- /dev/null +++ b/thanasoft-back/app/Http/Resources/FileAttachment/FileAttachmentResource.php @@ -0,0 +1,90 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'file_id' => $this->file_id, + 'label' => $this->label, + 'sort_order' => $this->sort_order, + 'attachable_type' => $this->attachable_type, + 'attachable_id' => $this->attachable_id, + 'created_at' => $this->created_at->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at->format('Y-m-d H:i:s'), + + // File information + 'file' => $this->whenLoaded('file', function () { + return [ + 'id' => $this->file->id, + 'name' => $this->file->name, + 'original_name' => $this->file->original_name ?? $this->file->name, + 'path' => $this->file->path, + 'mime_type' => $this->file->mime_type, + 'size' => $this->file->size, + 'size_formatted' => $this->formatFileSize($this->file->size ?? 0), + 'extension' => pathinfo($this->file->name, PATHINFO_EXTENSION), + 'download_url' => url('/api/files/' . $this->file->id . '/download'), + ]; + }), + + // Attachable model information + 'attachable' => $this->whenLoaded('attachable', function () { + return [ + 'id' => $this->attachable->id, + 'type' => class_basename($this->attachable), + 'name' => $this->getAttachableName(), + ]; + }), + + // Helper methods + 'is_for_intervention' => $this->isForIntervention(), + 'is_for_client' => $this->isForClient(), + 'is_for_deceased' => $this->isForDeceased(), + 'download_url' => $this->downloadUrl, + ]; + } + + /** + * Format file size in human readable format + */ + private function formatFileSize(int $bytes): string + { + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); + + $bytes /= (1 << (10 * $pow)); + + return round($bytes, 2) . ' ' . $units[$pow]; + } + + /** + * Get the display name of the attached model + */ + private function getAttachableName(): string + { + if (!$this->attachable) { + return 'Unknown'; + } + + return match (get_class($this->attachable)) { + \App\Models\Intervention::class => $this->attachable->title ?? "Intervention #{$this->attachable->id}", + \App\Models\Client::class => $this->attachable->name ?? "Client #{$this->attachable->id}", + \App\Models\Deceased::class => $this->attachable->name ?? "Deceased #{$this->attachable->id}", + default => 'Unknown Model' + }; + } +} diff --git a/thanasoft-back/app/Http/Resources/Fournisseur/FournisseurCollection.php b/thanasoft-back/app/Http/Resources/Fournisseur/FournisseurCollection.php new file mode 100644 index 0000000..fd5a894 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Fournisseur/FournisseurCollection.php @@ -0,0 +1,47 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'data' => $this->collection, + 'meta' => [ + 'total' => $this->total(), + 'per_page' => $this->perPage(), + 'current_page' => $this->currentPage(), + 'last_page' => $this->lastPage(), + 'from' => $this->firstItem(), + 'to' => $this->lastItem(), + 'stats' => [ + 'active' => $this->collection->where('is_active', true)->count(), + 'inactive' => $this->collection->where('is_active', false)->count(), + ], + ], + 'links' => [ + 'first' => $this->url(1), + 'last' => $this->url($this->lastPage()), + 'prev' => $this->previousPageUrl(), + 'next' => $this->nextPageUrl(), + ], + ]; + } + + public function with(Request $request): array + { + return [ + 'status' => 'success', + 'message' => 'Fournisseurs récupérés avec succès', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Fournisseur/FournisseurResource.php b/thanasoft-back/app/Http/Resources/Fournisseur/FournisseurResource.php new file mode 100644 index 0000000..b96145a --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Fournisseur/FournisseurResource.php @@ -0,0 +1,39 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'commercial' => $this->commercial(), + 'name' => $this->name, + 'vat_number' => $this->vat_number, + 'siret' => $this->siret, + 'email' => $this->email, + 'phone' => $this->phone, + 'billing_address' => [ + 'line1' => $this->billing_address_line1, + 'line2' => $this->billing_address_line2, + 'postal_code' => $this->billing_postal_code, + 'city' => $this->billing_city, + 'country_code' => $this->billing_country_code, + 'full_address' => $this->billing_address, + ], + 'notes' => $this->notes, + 'is_active' => $this->is_active, + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Fournisseur/PurchaseOrderResource.php b/thanasoft-back/app/Http/Resources/Fournisseur/PurchaseOrderResource.php new file mode 100644 index 0000000..dce42cc --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Fournisseur/PurchaseOrderResource.php @@ -0,0 +1,38 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'fournisseur_id' => $this->fournisseur_id, + 'fournisseur' => $this->whenLoaded('fournisseur'), + 'po_number' => $this->po_number, + 'status' => $this->status, + 'order_date' => $this->order_date ? $this->order_date->format('Y-m-d') : null, + 'expected_date' => $this->expected_date ? $this->expected_date->format('Y-m-d') : null, + 'currency' => $this->currency, + 'total_ht' => $this->total_ht, + 'total_tva' => $this->total_tva, + 'total_ttc' => $this->total_ttc, + 'notes' => $this->notes, + 'delivery_address' => $this->delivery_address, + 'lines' => $this->whenLoaded('lines'), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/GoodsReceiptLineResource.php b/thanasoft-back/app/Http/Resources/GoodsReceiptLineResource.php new file mode 100644 index 0000000..4df6241 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/GoodsReceiptLineResource.php @@ -0,0 +1,35 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'goods_receipt_id' => $this->goods_receipt_id, + 'product_id' => $this->product_id, + 'product' => new ProductResource($this->whenLoaded('product')), + 'packaging_id' => $this->packaging_id, + 'packaging' => new ProductPackagingResource($this->whenLoaded('packaging')), + 'packages_qty_received' => $this->packages_qty_received, + 'units_qty_received' => $this->units_qty_received, + 'qty_received_base' => $this->qty_received_base, + 'unit_price' => $this->unit_price, + 'unit_price_per_package' => $this->unit_price_per_package, + 'tva_rate_id' => $this->tva_rate_id, + 'tva_rate' => new TvaRateResource($this->whenLoaded('tvaRate')), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/GoodsReceiptResource.php b/thanasoft-back/app/Http/Resources/GoodsReceiptResource.php new file mode 100644 index 0000000..8e775bf --- /dev/null +++ b/thanasoft-back/app/Http/Resources/GoodsReceiptResource.php @@ -0,0 +1,33 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'purchase_order_id' => $this->purchase_order_id, + 'purchase_order' => new PurchaseOrderResource($this->whenLoaded('purchaseOrder')), + 'warehouse_id' => $this->warehouse_id, + 'warehouse' => new WarehouseResource($this->whenLoaded('warehouse')), + 'receipt_number' => $this->receipt_number, + 'receipt_date' => $this->receipt_date, + 'status' => $this->status, + 'notes' => $this->notes, + 'lines' => GoodsReceiptLineResource::collection($this->whenLoaded('lines')), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Intervention/InterventionAttachmentResource.php b/thanasoft-back/app/Http/Resources/Intervention/InterventionAttachmentResource.php new file mode 100644 index 0000000..ad4b398 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Intervention/InterventionAttachmentResource.php @@ -0,0 +1,33 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'intervention_id' => $this->intervention_id, + 'file' => $this->whenLoaded('file', function () { + return [ + 'id' => $this->file->id, + 'name' => $this->file->name, + 'path' => $this->file->path, + 'mime_type' => $this->file->mime_type, + 'size' => $this->file->size + ]; + }), + 'label' => $this->label, + 'created_at' => $this->created_at->format('Y-m-d H:i:s') + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Intervention/InterventionCollection.php b/thanasoft-back/app/Http/Resources/Intervention/InterventionCollection.php new file mode 100644 index 0000000..72fb989 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Intervention/InterventionCollection.php @@ -0,0 +1,46 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'data' => $this->collection, + 'meta' => [ + 'total' => $this->total(), + 'per_page' => $this->perPage(), + 'current_page' => $this->currentPage(), + 'last_page' => $this->lastPage(), + 'from' => $this->firstItem(), + 'to' => $this->lastItem(), + 'status_summary' => $this->calculateStatusSummary() + ] + ]; + } + + /** + * Calculate summary of intervention statuses. + * + * @return array + */ + protected function calculateStatusSummary(): array + { + $statusCounts = $this->collection->groupBy('status') + ->map(function ($group) { + return $group->count(); + }) + ->toArray(); + + return $statusCounts; + } +} diff --git a/thanasoft-back/app/Http/Resources/Intervention/InterventionNotificationResource.php b/thanasoft-back/app/Http/Resources/Intervention/InterventionNotificationResource.php new file mode 100644 index 0000000..dfd7da0 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Intervention/InterventionNotificationResource.php @@ -0,0 +1,28 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'intervention_id' => $this->intervention_id, + 'channel' => $this->channel, + 'destination' => $this->destination, + 'payload' => $this->payload, + 'status' => $this->status, + 'sent_at' => $this->sent_at ? $this->sent_at->format('Y-m-d H:i:s') : null, + 'created_at' => $this->created_at->format('Y-m-d H:i:s') + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Intervention/InterventionResource.php b/thanasoft-back/app/Http/Resources/Intervention/InterventionResource.php new file mode 100644 index 0000000..c0f8753 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Intervention/InterventionResource.php @@ -0,0 +1,97 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'quote_id' => $this->quote_id, + 'invoice_id' => $this->invoice_id, + 'quote' => $this->whenLoaded('quote', function () { + return new \App\Http\Resources\QuoteResource($this->quote); + }), + 'client' => $this->whenLoaded('client', function () { + return new ClientResource($this->client); + }), + 'deceased' => $this->whenLoaded('deceased', function () { + return new DeceasedResource($this->deceased); + }), + 'order_giver' => $this->order_giver, + 'location' => $this->whenLoaded('location', function () { + return [ + 'id' => $this->location->id, + 'name' => $this->location->name + ]; + }), + 'type' => $this->type, + 'scheduled_at' => $this->scheduled_at ? $this->scheduled_at->format('Y-m-d H:i:s') : null, + 'duration_min' => $this->duration_min, + 'status' => $this->status, + 'practitioners' => $this->whenLoaded('practitioners', function () { + return $this->practitioners->map(function ($practitioner) { + return [ + 'id' => $practitioner->id, + 'employee_id' => $practitioner->employee_id, + 'employee_name' => $practitioner->employee->full_name ?? ($practitioner->employee->first_name . ' ' . $practitioner->employee->last_name), + 'diploma_number' => $practitioner->diploma_number, + 'diploma_date' => $practitioner->diploma_date?->format('Y-m-d'), + 'authorization_number' => $practitioner->authorization_number, + 'authorization_issue_date' => $practitioner->authorization_issue_date?->format('Y-m-d'), + 'authorization_expiry_date' => $practitioner->authorization_expiry_date?->format('Y-m-d'), + 'notes' => $practitioner->notes, + 'is_authorization_valid' => $practitioner->is_authorization_valid, + 'created_at' => $practitioner->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $practitioner->updated_at?->format('Y-m-d H:i:s'), + 'role' => $practitioner->pivot->role ?? null, + ]; + }); + }), + 'principal_practitioner' => $this->whenLoaded('practitioners', function () { + $principal = $this->practitioners->where('pivot.role', 'principal')->first(); + if (!$principal) { + return null; + } + return [ + 'id' => $principal->id, + 'employee_id' => $principal->employee_id, + 'employee_name' => $principal->employee->full_name ?? ($principal->employee->first_name . ' ' . $principal->employee->last_name), + 'diploma_number' => $principal->diploma_number, + 'diploma_date' => $principal->diploma_date?->format('Y-m-d'), + 'authorization_number' => $principal->authorization_number, + 'authorization_issue_date' => $principal->authorization_issue_date?->format('Y-m-d'), + 'authorization_expiry_date' => $principal->authorization_expiry_date?->format('Y-m-d'), + 'notes' => $principal->notes, + 'is_authorization_valid' => $principal->is_authorization_valid, + 'created_at' => $principal->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $principal->updated_at?->format('Y-m-d H:i:s'), + 'role' => $principal->pivot->role ?? null, + ]; + }), + 'attachments_count' => $this->attachments_count, + 'notes' => $this->notes, + 'created_by' => $this->created_by, + 'created_at' => $this->created_at->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at->format('Y-m-d H:i:s'), + 'attachments' => $this->whenLoaded('attachments', function () { + return InterventionAttachmentResource::collection($this->attachments); + }), + 'notifications' => $this->whenLoaded('notifications', function () { + return InterventionNotificationResource::collection($this->notifications); + }) + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/InvoiceLineResource.php b/thanasoft-back/app/Http/Resources/InvoiceLineResource.php new file mode 100644 index 0000000..9797565 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/InvoiceLineResource.php @@ -0,0 +1,37 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'invoice_id' => $this->invoice_id, + 'product_id' => $this->product_id, + 'product_name' => $this->product ? $this->product->nom : null, + 'packaging_id' => $this->packaging_id, + 'packages_qty' => $this->packages_qty, + 'units_qty' => $this->units_qty, + 'description' => $this->description, + 'qty_base' => $this->qty_base, + 'unit_price' => $this->unit_price, + 'unit_price_per_package' => $this->unit_price_per_package, + 'tva_rate_id' => $this->tva_rate_id, + 'discount_pct' => $this->discount_pct, + 'total_ht' => $this->total_ht, + 'product' => $this->whenLoaded('product'), + 'packaging' => $this->whenLoaded('packaging'), + 'tva_rate' => $this->whenLoaded('tvaRate'), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/InvoiceResource.php b/thanasoft-back/app/Http/Resources/InvoiceResource.php new file mode 100644 index 0000000..9c953c8 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/InvoiceResource.php @@ -0,0 +1,41 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'client_id' => $this->client_id, + 'group_id' => $this->group_id, + 'source_quote_id' => $this->source_quote_id, + 'invoice_number' => $this->invoice_number, + 'status' => $this->status, + 'invoice_date' => $this->invoice_date, + 'due_date' => $this->due_date, + 'currency' => $this->currency, + 'total_ht' => $this->total_ht, + 'total_tva' => $this->total_tva, + 'total_ttc' => $this->total_ttc, + 'e_invoicing_channel_id' => $this->e_invoicing_channel_id, + 'e_invoice_status' => $this->e_invoice_status, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + 'client' => $this->whenLoaded('client'), + 'group' => $this->whenLoaded('group'), + 'sourceQuote' => $this->whenLoaded('sourceQuote'), + 'lines' => InvoiceLineResource::collection($this->whenLoaded('lines')), + 'history' => DocumentStatusHistoryResource::collection($this->whenLoaded('history')), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/PriceListResource.php b/thanasoft-back/app/Http/Resources/PriceListResource.php new file mode 100644 index 0000000..aae8112 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/PriceListResource.php @@ -0,0 +1,25 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'valid_from' => $this->valid_from?->format('Y-m-d'), + 'valid_to' => $this->valid_to?->format('Y-m-d'), + 'is_default' => (bool) $this->is_default, + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Product/ProductCollection.php b/thanasoft-back/app/Http/Resources/Product/ProductCollection.php new file mode 100644 index 0000000..a993bc4 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Product/ProductCollection.php @@ -0,0 +1,46 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'data' => $this->collection, + 'pagination' => [ + 'current_page' => $this->currentPage(), + 'from' => $this->firstItem(), + 'last_page' => $this->lastPage(), + 'per_page' => $this->perPage(), + 'to' => $this->lastItem(), + 'total' => $this->total(), + ], + 'summary' => [ + 'total_products' => $this->collection->count(), + 'low_stock_products' => $this->collection->filter(function ($product) { + return $product->stock_actuel <= $product->stock_minimum; + })->count(), + 'total_value' => $this->collection->sum(function ($product) { + return $product->stock_actuel * $product->prix_unitaire; + }), + ], + ]; + } + + public function with(Request $request): array + { + return [ + 'status' => 'success', + 'message' => 'Produits récupérés avec succès', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Product/ProductResource.php b/thanasoft-back/app/Http/Resources/Product/ProductResource.php new file mode 100644 index 0000000..30b0dcd --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Product/ProductResource.php @@ -0,0 +1,67 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'nom' => $this->nom, + 'reference' => $this->reference, + 'categorie_id' => $this->categorie_id, + 'fabricant' => $this->fabricant, + 'stock_actuel' => $this->stock_actuel, + 'stock_minimum' => $this->stock_minimum, + 'unite' => $this->unite, + 'prix_unitaire' => $this->prix_unitaire, + 'date_expiration' => $this->date_expiration?->format('Y-m-d'), + 'numero_lot' => $this->numero_lot, + 'conditionnement' => [ + 'nom' => $this->conditionnement_nom, + 'quantite' => $this->conditionnement_quantite, + 'unite' => $this->conditionnement_unite, + ], + 'media' => [ + 'photo_url' => $this->photo_url, + 'fiche_technique_url' => $this->fiche_technique_url, + ], + 'is_low_stock' => $this->stock_actuel <= $this->stock_minimum, + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + + // Relations + 'fournisseur' => $this->whenLoaded('fournisseur', function() { + return $this->fournisseur ? [ + 'id' => $this->fournisseur->id, + 'name' => $this->fournisseur->name, + 'email' => $this->fournisseur->email, + ] : null; + }), + 'category' => $this->whenLoaded('category', function() { + return $this->category ? [ + 'id' => $this->category->id, + 'name' => $this->category->name, + 'code' => $this->category->code, + ] : null; + }), + ]; + } + + public function with(Request $request): array + { + return [ + 'status' => 'success', + 'message' => 'Produit récupéré avec succès', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/ProductCategory/ProductCategoryCollection.php b/thanasoft-back/app/Http/Resources/ProductCategory/ProductCategoryCollection.php new file mode 100644 index 0000000..8fc42d1 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/ProductCategory/ProductCategoryCollection.php @@ -0,0 +1,36 @@ +resource instanceof \Illuminate\Pagination\LengthAwarePaginator) { + return [ + 'data' => ProductCategoryResource::collection($this->collection), + 'pagination' => [ + 'current_page' => $this->currentPage(), + 'from' => $this->firstItem(), + 'last_page' => $this->lastPage(), + 'per_page' => $this->perPage(), + 'to' => $this->lastItem(), + 'total' => $this->total(), + ], + ]; + } + + return [ + 'data' => ProductCategoryResource::collection($this->collection), + 'count' => $this->collection->count(), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/ProductCategory/ProductCategoryResource.php b/thanasoft-back/app/Http/Resources/ProductCategory/ProductCategoryResource.php new file mode 100644 index 0000000..ae45d76 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/ProductCategory/ProductCategoryResource.php @@ -0,0 +1,46 @@ + $this->id, + 'parent_id' => $this->parent_id, + 'code' => $this->code, + 'name' => $this->name, + 'description' => $this->description, + 'active' => $this->active, + 'path' => $this->path, + 'intervention'=> $this->intervention, + 'has_children' => $this->hasChildren(), + 'has_products' => $this->hasProducts(), + 'children_count' => $this->children()->count(), + 'products_count' => $this->products()->count(), + + // Parent information + 'parent' => $this->whenLoaded('parent', function () { + return new ProductCategoryResource($this->parent); + }), + + // Children information + 'children' => $this->whenLoaded('children', function () { + return ProductCategoryResource::collection($this->children); + }), + + // Relationships + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/ProductCategoryResource.php b/thanasoft-back/app/Http/Resources/ProductCategoryResource.php new file mode 100644 index 0000000..5632ded --- /dev/null +++ b/thanasoft-back/app/Http/Resources/ProductCategoryResource.php @@ -0,0 +1,31 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'parent_id' => $this->parent_id, + 'code' => $this->code, + 'name' => $this->name, + 'description' => $this->description, + 'intervention' => $this->intervention, + 'active' => $this->active, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + 'parent' => new ProductCategoryResource($this->whenLoaded('parent')), + 'children' => ProductCategoryResource::collection($this->whenLoaded('children')), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/ProductPackagingResource.php b/thanasoft-back/app/Http/Resources/ProductPackagingResource.php new file mode 100644 index 0000000..65d54e4 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/ProductPackagingResource.php @@ -0,0 +1,26 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'product_id' => $this->product_id, + 'name' => $this->name, + 'qty_base' => $this->qty_base, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/QuoteLineResource.php b/thanasoft-back/app/Http/Resources/QuoteLineResource.php new file mode 100644 index 0000000..a39ddbb --- /dev/null +++ b/thanasoft-back/app/Http/Resources/QuoteLineResource.php @@ -0,0 +1,37 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'quote_id' => $this->quote_id, + 'product_id' => $this->product_id, + 'product_name' => $this->product ? $this->product->nom : null, // Assuming 'nom' is the name field + 'packaging_id' => $this->packaging_id, + 'packages_qty' => $this->packages_qty, + 'units_qty' => $this->units_qty, + 'description' => $this->description, + 'qty_base' => $this->qty_base, + 'unit_price' => $this->unit_price, + 'unit_price_per_package' => $this->unit_price_per_package, + 'tva_rate_id' => $this->tva_rate_id, + 'discount_pct' => $this->discount_pct, + 'total_ht' => $this->total_ht, + 'product' => $this->whenLoaded('product'), + 'packaging' => $this->whenLoaded('packaging'), + 'tva_rate' => $this->whenLoaded('tvaRate'), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/QuoteResource.php b/thanasoft-back/app/Http/Resources/QuoteResource.php new file mode 100644 index 0000000..40202b2 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/QuoteResource.php @@ -0,0 +1,37 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'client_id' => $this->client_id, + 'group_id' => $this->group_id, + 'reference' => $this->reference, + 'status' => $this->status, + 'quote_date' => $this->quote_date, + 'valid_until' => $this->valid_until, + 'currency' => $this->currency, + 'total_ht' => $this->total_ht, + 'total_tva' => $this->total_tva, + 'total_ttc' => $this->total_ttc, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + 'client' => $this->whenLoaded('client'), + 'group' => $this->whenLoaded('group'), + 'lines' => QuoteLineResource::collection($this->whenLoaded('lines')), + 'history' => DocumentStatusHistoryResource::collection($this->whenLoaded('history')), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/StockItemResource.php b/thanasoft-back/app/Http/Resources/StockItemResource.php new file mode 100644 index 0000000..161084e --- /dev/null +++ b/thanasoft-back/app/Http/Resources/StockItemResource.php @@ -0,0 +1,30 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'product_id' => $this->product_id, + 'warehouse_id' => $this->warehouse_id, + 'qty_on_hand_base' => $this->qty_on_hand_base, + 'safety_stock_base' => $this->safety_stock_base, + 'product' => new ProductResource($this->whenLoaded('product')), + 'warehouse' => new WarehouseResource($this->whenLoaded('warehouse')), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/StockMoveResource.php b/thanasoft-back/app/Http/Resources/StockMoveResource.php new file mode 100644 index 0000000..a9559df --- /dev/null +++ b/thanasoft-back/app/Http/Resources/StockMoveResource.php @@ -0,0 +1,38 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'product_id' => $this->product_id, + 'from_warehouse_id' => $this->from_warehouse_id, + 'to_warehouse_id' => $this->to_warehouse_id, + 'packaging_id' => $this->packaging_id, + 'packages_qty' => $this->packages_qty, + 'units_qty' => $this->units_qty, + 'qty_base' => $this->qty_base, + 'move_type' => $this->move_type, + 'ref_type' => $this->ref_type, + 'ref_id' => $this->ref_id, + 'moved_at' => $this->moved_at, + 'product' => new ProductResource($this->whenLoaded('product')), + 'from_warehouse' => new WarehouseResource($this->whenLoaded('fromWarehouse')), + 'to_warehouse' => new WarehouseResource($this->whenLoaded('toWarehouse')), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/TvaRateResource.php b/thanasoft-back/app/Http/Resources/TvaRateResource.php new file mode 100644 index 0000000..834434c --- /dev/null +++ b/thanasoft-back/app/Http/Resources/TvaRateResource.php @@ -0,0 +1,25 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'rate' => $this->rate, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Vehicle/VehicleResource.php b/thanasoft-back/app/Http/Resources/Vehicle/VehicleResource.php new file mode 100644 index 0000000..002074a --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Vehicle/VehicleResource.php @@ -0,0 +1,42 @@ + $this->id, + 'photo' => [ + 'file_name' => $this->photo_file_name, + 'file_url' => $this->photo_file_url, + 'mime_type' => $this->photo_mime_type, + 'size' => $this->photo_size, + ], + 'brand' => $this->brand, + 'model' => $this->model, + 'registration_number' => $this->registration_number, + 'vehicle_type' => $this->vehicle_type, + 'fuel_type' => $this->fuel_type, + 'year' => $this->year, + 'status' => $this->status, + 'notes' => $this->notes, + 'primary_user_id' => $this->primary_user_id, + 'primary_user' => $this->whenLoaded('primaryUser', function () { + return $this->primaryUser ? [ + 'id' => $this->primaryUser->id, + 'first_name' => $this->primaryUser->first_name, + 'last_name' => $this->primaryUser->last_name, + 'full_name' => $this->primaryUser->full_name, + 'email' => $this->primaryUser->email, + ] : null; + }), + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/WarehouseResource.php b/thanasoft-back/app/Http/Resources/WarehouseResource.php new file mode 100644 index 0000000..d5487b2 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/WarehouseResource.php @@ -0,0 +1,29 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'address_line1' => $this->address_line1, + 'address_line2' => $this->address_line2, + 'postal_code' => $this->postal_code, + 'city' => $this->city, + 'country_code' => $this->country_code, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Webmail/UserMailboxSettingResource.php b/thanasoft-back/app/Http/Resources/Webmail/UserMailboxSettingResource.php new file mode 100644 index 0000000..3909c80 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Webmail/UserMailboxSettingResource.php @@ -0,0 +1,43 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'user_id' => $this->user_id, + 'imap_host' => $this->imap_host, + 'imap_port' => $this->imap_port, + 'imap_encryption' => $this->imap_encryption, + 'imap_validate_cert' => (bool) $this->imap_validate_cert, + 'imap_username' => $this->imap_username, + 'imap_folder' => $this->imap_folder, + 'imap_password_configured' => filled($this->imap_password), + 'smtp_host' => $this->smtp_host, + 'smtp_port' => $this->smtp_port, + 'smtp_encryption' => $this->smtp_encryption, + 'smtp_validate_cert' => (bool) $this->smtp_validate_cert, + 'smtp_username' => $this->smtp_username, + 'smtp_from_address' => $this->smtp_from_address, + 'smtp_from_name' => $this->smtp_from_name, + 'smtp_password_configured' => filled($this->smtp_password), + 'has_imap_configuration' => $this->hasImapConfiguration(), + 'has_smtp_configuration' => $this->hasSmtpConfiguration(), + 'last_synced_at' => $this->last_synced_at, + 'last_sync_error' => $this->last_sync_error, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Http/Resources/Webmail/WebmailMessageResource.php b/thanasoft-back/app/Http/Resources/Webmail/WebmailMessageResource.php new file mode 100644 index 0000000..eee4c58 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Webmail/WebmailMessageResource.php @@ -0,0 +1,44 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'user_id' => $this->user_id, + 'message_uid' => $this->message_uid, + 'direction' => $this->direction, + 'folder' => $this->folder, + 'status' => $this->status, + 'from_email' => $this->from_email, + 'from_name' => $this->from_name, + 'to' => $this->to_recipients ?? [], + 'cc' => $this->cc_recipients ?? [], + 'bcc' => $this->bcc_recipients ?? [], + 'subject' => $this->subject, + 'body' => $this->body, + 'snippet' => $this->snippet, + 'attachments' => $this->attachments ?? [], + 'metadata' => $this->metadata ?? [], + 'is_read' => $this->read_at !== null, + 'is_starred' => $this->starred_at !== null, + 'read_at' => $this->read_at, + 'starred_at' => $this->starred_at, + 'sent_at' => $this->sent_at, + 'received_at' => $this->received_at, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Mail/DocumentMail.php b/thanasoft-back/app/Mail/DocumentMail.php new file mode 100644 index 0000000..dc6a3ff --- /dev/null +++ b/thanasoft-back/app/Mail/DocumentMail.php @@ -0,0 +1,70 @@ +document = $document; + $this->type = $type; + $this->pdfContent = $pdfContent; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + $subject = $this->type === 'quote' + ? "Votre devis Thanasoft : " . $this->document->reference + : "Votre facture Thanasoft : " . $this->document->invoice_number; + + return new Envelope( + subject: $subject, + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + view: 'emails.document', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + $filename = $this->type === 'quote' + ? 'Devis_' . $this->document->reference . '.pdf' + : 'Facture_' . $this->document->invoice_number . '.pdf'; + + return [ + Attachment::fromData(fn () => $this->pdfContent, $filename) + ->withMime('application/pdf'), + ]; + } +} diff --git a/thanasoft-back/app/Mail/WebmailMessageMail.php b/thanasoft-back/app/Mail/WebmailMessageMail.php new file mode 100644 index 0000000..55ca6e0 --- /dev/null +++ b/thanasoft-back/app/Mail/WebmailMessageMail.php @@ -0,0 +1,40 @@ + $payload + */ + public function __construct(public array $payload) + { + } + + public function envelope(): Envelope + { + return new Envelope( + subject: $this->payload['subject'] ?? 'Nouveau message', + ); + } + + public function content(): Content + { + return new Content( + view: 'emails.webmail_message', + with: [ + 'body' => $this->payload['body'] ?? '', + ], + ); + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Models/Avoir.php b/thanasoft-back/app/Models/Avoir.php new file mode 100644 index 0000000..6257d6e --- /dev/null +++ b/thanasoft-back/app/Models/Avoir.php @@ -0,0 +1,98 @@ + 'date', + 'due_date' => 'date', + 'refund_date' => 'date', + 'total_ht' => 'decimal:2', + 'total_tva' => 'decimal:2', + 'total_ttc' => 'decimal:2', + 'compensation_amount' => 'decimal:2', + ]; + + protected static function booted() + { + static::creating(function ($avoir) { + // Auto-generate avoir number if not provided + if (empty($avoir->avoir_number)) { + $prefix = 'AV-' . now()->format('Ym') . '-'; + $lastAvoir = self::where('avoir_number', 'like', $prefix . '%') + ->orderBy('avoir_number', 'desc') + ->first(); + + if ($lastAvoir) { + // Extract numeric part + preg_match('/(\d+)$/', $lastAvoir->avoir_number, $matches); + $newNumber = $matches ? intval($matches[1]) + 1 : 1; + } else { + $newNumber = 1; + } + + $avoir->avoir_number = $prefix . str_pad((string)$newNumber, 4, '0', STR_PAD_LEFT); + } + }); + } + + public function client() + { + return $this->belongsTo(Client::class); + } + + public function invoice() + { + return $this->belongsTo(Invoice::class); + } + + public function group() + { + return $this->belongsTo(ClientGroup::class, 'group_id'); + } + + public function lines() + { + return $this->hasMany(AvoirLine::class); + } + + public function compensationInvoice() + { + return $this->belongsTo(Invoice::class, 'compensation_invoice_id'); + } + + public function history() + { + return $this->hasMany(DocumentStatusHistory::class, 'document_id') + ->where('document_type', 'avoir') + ->orderBy('changed_at', 'desc'); + } +} diff --git a/thanasoft-back/app/Models/AvoirLine.php b/thanasoft-back/app/Models/AvoirLine.php new file mode 100644 index 0000000..bd7e691 --- /dev/null +++ b/thanasoft-back/app/Models/AvoirLine.php @@ -0,0 +1,49 @@ + 'decimal:3', + 'unit_price' => 'decimal:4', + 'tva_rate' => 'decimal:2', + 'total_ht' => 'decimal:2', + 'total_tva' => 'decimal:2', + 'total_ttc' => 'decimal:2', + ]; + + public function avoir() + { + return $this->belongsTo(Avoir::class); + } + + public function product() + { + return $this->belongsTo(Product::class); + } + + public function invoiceLine() + { + return $this->belongsTo(InvoiceLine::class); + } +} diff --git a/thanasoft-back/app/Models/Client.php b/thanasoft-back/app/Models/Client.php new file mode 100644 index 0000000..2e7ad2f --- /dev/null +++ b/thanasoft-back/app/Models/Client.php @@ -0,0 +1,112 @@ + 'boolean', + 'is_parent' => 'boolean', + ]; + + public function parent(): BelongsTo + { + return $this->belongsTo(Client::class, 'parent_id'); + } + + public function children() + { + return $this->hasMany(Client::class, 'parent_id'); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function commercial(): ?string + { + return $this->user ? $this->user->name : 'Système'; + } + + public function category(): BelongsTo + { + return $this->belongsTo(ClientCategory::class, 'client_category_id'); + } + + public function group(): BelongsTo + { + return $this->belongsTo(ClientGroup::class, 'group_id'); + } + + public function convoys() + { + return $this->hasMany(Convoy::class); + } + + /** + * Get the human-readable label for the client type. + */ + public function getTypeLabel(): string + { + return $this->category ? $this->category->name : 'Non catégorisé'; + } + + /** + * Get the full billing address as a string. + */ + public function getBillingAddressAttribute(): ?string + { + $parts = array_filter([ + $this->billing_address_line1, + $this->billing_address_line2, + $this->billing_postal_code ? $this->billing_postal_code . ' ' . $this->billing_city : $this->billing_city, + $this->billing_country_code, + ]); + + return !empty($parts) ? implode(', ', $parts) : null; + } + + /** + * Get the file attachments for the client (polymorphic). + */ + public function fileAttachments() + { + return $this->morphMany(FileAttachment::class, 'attachable')->orderBy('sort_order'); + } + + /** + * Get the files attached to this client. + */ + public function attachedFiles() + { + return $this->fileAttachments()->with('file'); + } +} diff --git a/thanasoft-back/app/Models/ClientActivityTimeline.php b/thanasoft-back/app/Models/ClientActivityTimeline.php new file mode 100644 index 0000000..d38fbb3 --- /dev/null +++ b/thanasoft-back/app/Models/ClientActivityTimeline.php @@ -0,0 +1,47 @@ + 'array', + 'created_at' => 'datetime', + ]; + + public function client() + { + return $this->belongsTo(Client::class); + } + + public function actor() + { + return $this->belongsTo(User::class, 'actor_user_id'); + } + + // Polymorphic relation to entity if we had models for all of them + // public function entity() + // { + // return $this->morphTo(); + // } +} diff --git a/thanasoft-back/app/Models/ClientCategory.php b/thanasoft-back/app/Models/ClientCategory.php new file mode 100644 index 0000000..1e186fd --- /dev/null +++ b/thanasoft-back/app/Models/ClientCategory.php @@ -0,0 +1,29 @@ + 'boolean', + ]; + + /** + * Get the clients for the category. + */ + public function clients(): HasMany + { + return $this->hasMany(Client::class); + } +} diff --git a/thanasoft-back/app/Models/ClientContact.php b/thanasoft-back/app/Models/ClientContact.php new file mode 100644 index 0000000..20b6cc0 --- /dev/null +++ b/thanasoft-back/app/Models/ClientContact.php @@ -0,0 +1,10 @@ +belongsTo(PriceList::class, 'price_list_id'); + } + + public function clients(): HasMany + { + return $this->hasMany(Client::class, 'group_id'); + } +} diff --git a/thanasoft-back/app/Models/ClientLocation.php b/thanasoft-back/app/Models/ClientLocation.php new file mode 100644 index 0000000..498edb1 --- /dev/null +++ b/thanasoft-back/app/Models/ClientLocation.php @@ -0,0 +1,27 @@ + 'boolean', + 'gps_lat' => 'decimal:8', + 'gps_lng' => 'decimal:8', + ]; +} diff --git a/thanasoft-back/app/Models/Contact.php b/thanasoft-back/app/Models/Contact.php new file mode 100644 index 0000000..5511f8f --- /dev/null +++ b/thanasoft-back/app/Models/Contact.php @@ -0,0 +1,44 @@ + 'boolean', + ]; + + public function client(): BelongsTo + { + return $this->belongsTo(Client::class); + } + + public function fournisseur(): BelongsTo + { + return $this->belongsTo(Fournisseur::class); + } + + /** + * Get the contact's full name. + */ + public function getFullNameAttribute(): string + { + return trim("{$this->first_name} {$this->last_name}"); + } +} diff --git a/thanasoft-back/app/Models/Convoy.php b/thanasoft-back/app/Models/Convoy.php new file mode 100644 index 0000000..4683ced --- /dev/null +++ b/thanasoft-back/app/Models/Convoy.php @@ -0,0 +1,64 @@ + 'datetime', + 'estimated_end_at' => 'datetime', + 'automatic_notifications' => 'boolean', + 'departure_latitude' => 'decimal:7', + 'departure_longitude' => 'decimal:7', + ]; + + public function deceased(): BelongsTo + { + return $this->belongsTo(Deceased::class); + } + + public function client(): BelongsTo + { + return $this->belongsTo(Client::class); + } + + public function vehicle(): BelongsTo + { + return $this->belongsTo(Vehicle::class); + } + + public function departureLocation(): BelongsTo + { + return $this->belongsTo(ClientLocation::class, 'departure_location_id'); + } +} diff --git a/thanasoft-back/app/Models/Deceased.php b/thanasoft-back/app/Models/Deceased.php new file mode 100644 index 0000000..51ce87e --- /dev/null +++ b/thanasoft-back/app/Models/Deceased.php @@ -0,0 +1,80 @@ + 'date', + 'death_date' => 'date', + ]; + + /** + * Get the documents associated with the deceased. + */ + public function documents(): HasMany + { + return $this->hasMany(DeceasedDocument::class); + } + + /** + * Get the interventions associated with the deceased. + */ + public function interventions(): HasMany + { + return $this->hasMany(Intervention::class); + } + + public function convoys(): HasMany + { + return $this->hasMany(Convoy::class); + } + + /** + * Get the file attachments for the deceased (polymorphic). + */ + public function fileAttachments() + { + return $this->morphMany(FileAttachment::class, 'attachable')->orderBy('sort_order'); + } + + /** + * Get the files attached to this deceased. + */ + public function attachedFiles() + { + return $this->fileAttachments()->with('file'); + } +} diff --git a/thanasoft-back/app/Models/DeceasedDocument.php b/thanasoft-back/app/Models/DeceasedDocument.php new file mode 100644 index 0000000..b7b15ac --- /dev/null +++ b/thanasoft-back/app/Models/DeceasedDocument.php @@ -0,0 +1,49 @@ + 'datetime' + ]; + + /** + * Get the deceased associated with the document. + */ + public function deceased(): BelongsTo + { + return $this->belongsTo(Deceased::class); + } + + /** + * Get the file associated with the document. + */ + public function file(): BelongsTo + { + return $this->belongsTo(File::class); + } +} diff --git a/thanasoft-back/app/Models/DocumentStatusHistory.php b/thanasoft-back/app/Models/DocumentStatusHistory.php new file mode 100644 index 0000000..d11eb35 --- /dev/null +++ b/thanasoft-back/app/Models/DocumentStatusHistory.php @@ -0,0 +1,44 @@ + 'datetime', + ]; + + public function user() + { + return $this->belongsTo(User::class, 'changed_by'); + } + + /** + * Get the parent document model (quote or invoice). + */ + public function document() + { + // define a custom polymorphic relationship or helper if needed + // Since it is an enum, we can't use standard morphTo easily without a map. + // But for now, I will just leave it or maybe add a helper. + // Standard Laravel morph expects 'document_type' to be the class name. + // Here it is 'quote' or 'invoice'. + // We can use morphMap in AppServiceProvider to map 'quote' => Quote::class. + return $this->morphTo(__FUNCTION__, 'document_type', 'document_id'); + } +} diff --git a/thanasoft-back/app/Models/Employee.php b/thanasoft-back/app/Models/Employee.php new file mode 100644 index 0000000..5adb6f5 --- /dev/null +++ b/thanasoft-back/app/Models/Employee.php @@ -0,0 +1,97 @@ + + */ + protected $fillable = [ + 'first_name', + 'last_name', + 'user_id', + 'email', + 'phone', + 'job_title', + 'hire_date', + 'active', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'active' => 'boolean', + 'hire_date' => 'date', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * Get the thanatopractitioner associated with the employee. + */ + public function thanatopractitioner(): HasOne + { + return $this->hasOne(Thanatopractitioner::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function vehicles(): HasMany + { + return $this->hasMany(Vehicle::class, 'primary_user_id'); + } + + /** + * Get the full name of the employee. + */ + public function getFullNameAttribute(): string + { + return $this->first_name . ' ' . $this->last_name; + } + + /** + * Scope a query to only include active employees. + */ + public function scopeActive($query) + { + return $query->where('active', true); + } + + /** + * Scope a query to only include inactive employees. + */ + public function scopeInactive($query) + { + return $query->where('active', false); + } + + /** + * Scope a query to search employees. + */ + public function scopeSearch($query, string $term) + { + return $query->where(function ($q) use ($term) { + $q->where('first_name', 'like', '%' . $term . '%') + ->orWhere('last_name', 'like', '%' . $term . '%') + ->orWhere('email', 'like', '%' . $term . '%') + ->orWhere('job_title', 'like', '%' . $term . '%'); + }); + } +} diff --git a/thanasoft-back/app/Models/File.php b/thanasoft-back/app/Models/File.php new file mode 100644 index 0000000..7124946 --- /dev/null +++ b/thanasoft-back/app/Models/File.php @@ -0,0 +1,98 @@ + 'integer', + 'uploaded_at' => 'datetime', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'uploaded_by'); + } + + /** + * Get the uploader name. + */ + public function getUploaderName(): string + { + return $this->user ? $this->user->name : 'Utilisateur inconnu'; + } + + /** + * Get the formatted file size. + */ + public function getFormattedSize(): string + { + if (!$this->size_bytes) { + return '0 B'; + } + + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + $bytes = $this->size_bytes; + $i = 0; + + while ($bytes >= 1024 && $i < count($units) - 1) { + $bytes /= 1024; + $i++; + } + + return round($bytes, 2) . ' ' . $units[$i]; + } + + /** + * Get the file extension from the file name. + */ + public function getExtension(): string + { + return pathinfo($this->file_name, PATHINFO_EXTENSION); + } + + /** + * Check if the file is an image. + */ + public function isImage(): bool + { + return str_starts_with($this->mime_type ?? '', 'image/'); + } + + /** + * Check if the file is a PDF. + */ + public function isPdf(): bool + { + return $this->mime_type === 'application/pdf'; + } + + /** + * Get the organized storage path (e.g., client/devis/filename.pdf). + */ + public function getOrganizedPath(): string + { + // Extract directory structure from storage_uri + $path = $this->storage_uri; + + // Remove storage path prefix if present + if (str_contains($path, 'storage/')) { + $path = substr($path, strpos($path, 'storage/') + 8); + } + + return $path; + } +} diff --git a/thanasoft-back/app/Models/FileAttachment.php b/thanasoft-back/app/Models/FileAttachment.php new file mode 100644 index 0000000..a85c39f --- /dev/null +++ b/thanasoft-back/app/Models/FileAttachment.php @@ -0,0 +1,141 @@ + 'integer', + ]; + + /** + * Get the parent attachable model (polymorphic). + */ + public function attachable(): MorphTo + { + return $this->morphTo(); + } + + /** + * Get the file associated with the attachment. + */ + public function file(): BelongsTo + { + return $this->belongsTo(File::class); + } + + /** + * Get the intervention associated with the attachment (legacy support). + */ + public function intervention(): BelongsTo + { + return $this->belongsTo(Intervention::class, 'attachable_id')->where('attachable_type', Intervention::class); + } + + /** + * Get the client associated with the attachment. + */ + public function client(): BelongsTo + { + return $this->belongsTo(Client::class, 'attachable_id')->where('attachable_type', Client::class); + } + + /** + * Get the deceased associated with the attachment. + */ + public function deceased(): BelongsTo + { + return $this->belongsTo(Deceased::class, 'attachable_id')->where('attachable_type', Deceased::class); + } + + /** + * Scope to filter by attachable type. + */ + public function scopeOfType($query, string $type) + { + return $query->where('attachable_type', $type); + } + + /** + * Scope to filter by attachable model. + */ + public function scopeFor($query, Model $model) + { + return $query->where('attachable_type', get_class($model)) + ->where('attachable_id', $model->getKey()); + } + + /** + * Get the display name of the attached model. + */ + public function getAttachableNameAttribute(): string + { + return $this->attachable?->name ?? $this->attachable?->file_name ?? 'Unknown'; + } + + /** + * Get the URL for downloading the attached file. + */ + public function getDownloadUrlAttribute(): string + { + return url('/api/files/' . $this->file_id . '/download'); + } + + /** + * Check if this attachment belongs to an intervention. + */ + public function isForIntervention(): bool + { + return $this->attachable_type === Intervention::class; + } + + /** + * Check if this attachment belongs to a client. + */ + public function isForClient(): bool + { + return $this->attachable_type === Client::class; + } + + /** + * Check if this attachment belongs to a deceased. + */ + public function isForDeceased(): bool + { + return $this->attachable_type === Deceased::class; + } +} diff --git a/thanasoft-back/app/Models/Fournisseur.php b/thanasoft-back/app/Models/Fournisseur.php new file mode 100644 index 0000000..af40b76 --- /dev/null +++ b/thanasoft-back/app/Models/Fournisseur.php @@ -0,0 +1,60 @@ + 'boolean', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function products(): HasMany + { + return $this->hasMany(Product::class); + } + + public function commercial(): ?string + { + return $this->user ? $this->user->name : 'Système'; + } + + /** + * Get the full billing address as a string. + */ + public function getBillingAddressAttribute(): ?string + { + $parts = array_filter([ + $this->billing_address_line1, + $this->billing_address_line2, + $this->billing_postal_code ? $this->billing_postal_code . ' ' . $this->billing_city : $this->billing_city, + $this->billing_country_code, + ]); + + return !empty($parts) ? implode(', ', $parts) : null; + } +} diff --git a/thanasoft-back/app/Models/GoodsReceipt.php b/thanasoft-back/app/Models/GoodsReceipt.php new file mode 100644 index 0000000..326ba59 --- /dev/null +++ b/thanasoft-back/app/Models/GoodsReceipt.php @@ -0,0 +1,41 @@ + 'date', + ]; + + public function purchaseOrder(): BelongsTo + { + return $this->belongsTo(PurchaseOrder::class); + } + + public function warehouse(): BelongsTo + { + return $this->belongsTo(Warehouse::class); + } + + public function lines(): HasMany + { + return $this->hasMany(GoodsReceiptLine::class); + } +} diff --git a/thanasoft-back/app/Models/GoodsReceiptLine.php b/thanasoft-back/app/Models/GoodsReceiptLine.php new file mode 100644 index 0000000..f9e48a3 --- /dev/null +++ b/thanasoft-back/app/Models/GoodsReceiptLine.php @@ -0,0 +1,53 @@ + 'decimal:3', + 'units_qty_received' => 'decimal:3', + 'qty_received_base' => 'decimal:3', + 'unit_price' => 'decimal:2', + 'unit_price_per_package' => 'decimal:2', + ]; + + public function goodsReceipt(): BelongsTo + { + return $this->belongsTo(GoodsReceipt::class); + } + + public function product(): BelongsTo + { + return $this->belongsTo(Product::class); + } + + public function packaging(): BelongsTo + { + return $this->belongsTo(ProductPackaging::class); + } + + public function tvaRate(): BelongsTo + { + return $this->belongsTo(TvaRate::class); + } +} diff --git a/thanasoft-back/app/Models/Intervention.php b/thanasoft-back/app/Models/Intervention.php new file mode 100644 index 0000000..c69bc31 --- /dev/null +++ b/thanasoft-back/app/Models/Intervention.php @@ -0,0 +1,168 @@ + 'datetime', + 'attachments_count' => 'integer' + ]; + + /** + * Get the product associated with the intervention. + */ + public function product(): BelongsTo + { + return $this->belongsTo(Product::class); + } + + /** + * Get the client associated with the intervention. + */ + public function client(): BelongsTo + { + return $this->belongsTo(Client::class); + } + + /** + * Get the deceased associated with the intervention. + */ + public function deceased(): BelongsTo + { + return $this->belongsTo(Deceased::class); + } + + /** + * Get the location associated with the intervention. + */ + public function location(): BelongsTo + { + return $this->belongsTo(ClientLocation::class); + } + + /** + * Get the practitioners assigned to the intervention. + */ + public function practitioners(): BelongsToMany + { + return $this->belongsToMany(Thanatopractitioner::class , 'intervention_practitioner', 'intervention_id', 'practitioner_id') + ->withPivot('role', 'assigned_at') + ->withTimestamps(); + } + + /** + * Alias for practitioners relationship (for backward compatibility). + */ + public function assignedPractitioner(): BelongsToMany + { + return $this->practitioners(); + } + + /** + * Get the principal practitioner assigned to the intervention. + */ + public function principalPractitioner(): BelongsToMany + { + return $this->belongsToMany(Thanatopractitioner::class , 'intervention_practitioner', 'intervention_id', 'practitioner_id') + ->wherePivot('role', 'principal') + ->withPivot('role', 'assigned_at') + ->withTimestamps(); + } + + /** + * Get the assistant practitioners assigned to the intervention. + */ + public function assistantPractitioners(): BelongsToMany + { + return $this->belongsToMany(Thanatopractitioner::class , 'intervention_practitioner', 'intervention_id', 'practitioner_id') + ->wherePivot('role', 'assistant') + ->withPivot('role', 'assigned_at') + ->withTimestamps(); + } + + /** + * Get the user who created the intervention. + */ + public function creator(): BelongsTo + { + return $this->belongsTo(User::class , 'created_by'); + } + + /** + * Get the attachments for the intervention (legacy support). + */ + public function attachments(): HasMany + { + return $this->hasMany(InterventionAttachment::class); + } + + /** + * Get the file attachments for the intervention (polymorphic). + */ + public function fileAttachments() + { + return $this->morphMany(FileAttachment::class , 'attachable')->orderBy('sort_order'); + } + + /** + * Get the files attached to this intervention. + */ + public function attachedFiles() + { + return $this->fileAttachments()->with('file'); + } + + /** + * Get the notifications for the intervention. + */ + public function notifications(): HasMany + { + return $this->hasMany(InterventionNotification::class); + } + + public function invoice(): BelongsTo + { + return $this->belongsTo(Invoice::class); + } + + public function quote(): BelongsTo + { + return $this->belongsTo(Quote::class); + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Models/InterventionAttachment.php b/thanasoft-back/app/Models/InterventionAttachment.php new file mode 100644 index 0000000..f0f939d --- /dev/null +++ b/thanasoft-back/app/Models/InterventionAttachment.php @@ -0,0 +1,39 @@ +belongsTo(Intervention::class); + } + + /** + * Get the file associated with the attachment. + */ + public function file(): BelongsTo + { + return $this->belongsTo(File::class); + } +} diff --git a/thanasoft-back/app/Models/InterventionNotification.php b/thanasoft-back/app/Models/InterventionNotification.php new file mode 100644 index 0000000..63a9800 --- /dev/null +++ b/thanasoft-back/app/Models/InterventionNotification.php @@ -0,0 +1,44 @@ + 'array', + 'sent_at' => 'datetime' + ]; + + /** + * Get the intervention associated with the notification. + */ + public function intervention(): BelongsTo + { + return $this->belongsTo(Intervention::class); + } +} diff --git a/thanasoft-back/app/Models/InterventionPractitioner.php b/thanasoft-back/app/Models/InterventionPractitioner.php new file mode 100644 index 0000000..418dc1b --- /dev/null +++ b/thanasoft-back/app/Models/InterventionPractitioner.php @@ -0,0 +1,72 @@ + 'datetime' + ]; + + /** + * Get the intervention that owns the practitioner assignment. + */ + public function intervention(): BelongsTo + { + return $this->belongsTo(Intervention::class); + } + + /** + * Get the practitioner assigned to the intervention. + */ + public function practitioner(): BelongsTo + { + return $this->belongsTo(Thanatopractitioner::class, 'practitioner_id'); + } + + /** + * Scope to get principal practitioners. + */ + public function scopePrincipal($query) + { + return $query->where('role', 'principal'); + } + + /** + * Scope to get assistant practitioners. + */ + public function scopeAssistant($query) + { + return $query->where('role', 'assistant'); + } +} diff --git a/thanasoft-back/app/Models/Invoice.php b/thanasoft-back/app/Models/Invoice.php new file mode 100644 index 0000000..0a27cd9 --- /dev/null +++ b/thanasoft-back/app/Models/Invoice.php @@ -0,0 +1,89 @@ +invoice_number)) { + $prefix = 'FAC-' . now()->format('Ym') . '-'; + $lastInvoice = self::where('invoice_number', 'like', $prefix . '%') + ->orderBy('invoice_number', 'desc') + ->first(); + + if ($lastInvoice) { + $lastNumber = intval(substr($lastInvoice->invoice_number, -4)); + $newNumber = $lastNumber + 1; + } else { + $newNumber = 1; + } + + $invoice->invoice_number = $prefix . str_pad((string)$newNumber, 4, '0', STR_PAD_LEFT); + } + }); + } + + protected $casts = [ + 'invoice_date' => 'date', + 'due_date' => 'date', + 'total_ht' => 'decimal:2', + 'total_tva' => 'decimal:2', + 'total_ttc' => 'decimal:2', + ]; + + public function client() + { + return $this->belongsTo(Client::class); + } + + public function group() + { + return $this->belongsTo(ClientGroup::class, 'group_id'); + } + + public function lines() + { + return $this->hasMany(InvoiceLine::class); + } + + public function sourceQuote() + { + return $this->belongsTo(Quote::class, 'source_quote_id'); + } + + public function eInvoicingChannel() + { + return $this->belongsTo(EInvoicingChannel::class); + } + + public function history() + { + return $this->hasMany(DocumentStatusHistory::class, 'document_id') + ->where('document_type', 'invoice') + ->orderBy('changed_at', 'desc'); + } +} diff --git a/thanasoft-back/app/Models/InvoiceLine.php b/thanasoft-back/app/Models/InvoiceLine.php new file mode 100644 index 0000000..c5b820d --- /dev/null +++ b/thanasoft-back/app/Models/InvoiceLine.php @@ -0,0 +1,48 @@ +belongsTo(Invoice::class); + } + + public function product() + { + return $this->belongsTo(Product::class); + } + + public function packaging() + { + return $this->belongsTo(\App\Models\Stock\ProductPackaging::class, 'packaging_id'); + } + + public function tvaRate() + { + return $this->belongsTo(\App\Models\TvaRate::class, 'tva_rate_id'); + } +} diff --git a/thanasoft-back/app/Models/PractitionerDocument.php b/thanasoft-back/app/Models/PractitionerDocument.php new file mode 100644 index 0000000..66d1f9d --- /dev/null +++ b/thanasoft-back/app/Models/PractitionerDocument.php @@ -0,0 +1,86 @@ + + */ + protected $fillable = [ + 'practitioner_id', + 'doc_type', + 'file_id', + 'issue_date', + 'expiry_date', + 'status', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'issue_date' => 'date', + 'expiry_date' => 'date', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * Get the thanatopractitioner that owns the document. + */ + public function thanatopractitioner(): BelongsTo + { + return $this->belongsTo(Thanatopractitioner::class, 'practitioner_id'); + } + + /** + * Scope a query to only include documents with valid expiry date. + */ + public function scopeValid($query) + { + return $query->where(function ($q) { + $q->whereNull('expiry_date') + ->orWhere('expiry_date', '>=', now()); + }); + } + + /** + * Scope a query to only include documents with expired expiry date. + */ + public function scopeExpired($query) + { + return $query->whereNotNull('expiry_date') + ->where('expiry_date', '<', now()); + } + + /** + * Scope a query to filter by document type. + */ + public function scopeOfType($query, string $type) + { + return $query->where('doc_type', $type); + } + + /** + * Check if the document is still valid. + */ + public function getIsValidAttribute(): bool + { + if (!$this->expiry_date) { + return true; // No expiry date means it's valid + } + + return $this->expiry_date >= now(); + } +} diff --git a/thanasoft-back/app/Models/PriceList.php b/thanasoft-back/app/Models/PriceList.php new file mode 100644 index 0000000..735d341 --- /dev/null +++ b/thanasoft-back/app/Models/PriceList.php @@ -0,0 +1,36 @@ + 'date', + 'valid_to' => 'date', + 'is_default' => 'boolean', + ]; + + public function clientGroups(): HasMany + { + return $this->hasMany(ClientGroup::class, 'price_list_id'); + } + + public function products(): BelongsToMany + { + return $this->belongsToMany(Product::class, 'product_price_list') + ->withPivot('price') + ->withTimestamps(); + } +} + diff --git a/thanasoft-back/app/Models/Product.php b/thanasoft-back/app/Models/Product.php new file mode 100644 index 0000000..758c50e --- /dev/null +++ b/thanasoft-back/app/Models/Product.php @@ -0,0 +1,157 @@ + 'decimal:2', + 'stock_minimum' => 'decimal:2', + 'prix_unitaire' => 'decimal:2', + 'conditionnement_quantite' => 'decimal:2', + 'date_expiration' => 'date', + ]; + + /** + * Get the fournisseur that owns the product. + */ + public function fournisseur(): BelongsTo + { + return $this->belongsTo(Fournisseur::class); + } + + /** + * Get the category that owns the product. + */ + public function category(): BelongsTo + { + return $this->belongsTo(ProductCategory::class, 'categorie_id'); + } + + /** + * Handle image upload + */ + public function uploadImage($image) + { + if ($image) { + // Delete old image if exists + if ($this->image) { + $this->deleteImage(); + } + + // Store the new image + $imageName = time() . '_' . uniqid() . '.' . $image->getClientOriginalExtension(); + $imagePath = $image->storeAs('products', $imageName, 'public'); + + $this->image = $imagePath; + $this->save(); + + return $imagePath; + } + + return null; + } + + /** + * Delete the product image + */ + public function deleteImage() + { + if ($this->image) { + Storage::disk('public')->delete($this->image); + $this->image = null; + $this->save(); + } + } + + /** + * Get the full URL of the image + */ + public function getImageUrlAttribute() + { + if ($this->image) { + return Storage::url($this->image); + } + + return null; + } + + /** + * Get the stock items for the product. + */ + public function stockItems(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(StockItem::class); + } + + /** + * Get the packagings for the product. + */ + public function packagings(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(\App\Models\Stock\ProductPackaging::class); + } + + /** + * Get the stock moves for the product. + */ + public function stockMoves(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(StockMove::class); + } + + /** + * Get the goods receipt lines for the product. + */ + public function goodsReceiptLines(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(GoodsReceiptLine::class); + } + + /** + * Price lists attached to this product with custom price. + */ + public function priceLists(): BelongsToMany + { + return $this->belongsToMany(PriceList::class, 'product_price_list') + ->withPivot('price') + ->withTimestamps(); + } + + /** + * Boot the model + */ + protected static function boot() + { + parent::boot(); + + static::deleting(function ($product) { + // Delete the image when the product is deleted + $product->deleteImage(); + }); + } +} diff --git a/thanasoft-back/app/Models/ProductCategory.php b/thanasoft-back/app/Models/ProductCategory.php new file mode 100644 index 0000000..a0cf4d8 --- /dev/null +++ b/thanasoft-back/app/Models/ProductCategory.php @@ -0,0 +1,83 @@ + 'boolean', + 'intervention' => 'boolean', + ]; + + /** + * Get the parent category. + */ + public function parent(): BelongsTo + { + return $this->belongsTo(ProductCategory::class, 'parent_id'); + } + + /** + * Get the child categories. + */ + public function children(): HasMany + { + return $this->hasMany(ProductCategory::class, 'parent_id'); + } + + /** + * Scope a query to only include active categories. + */ + public function scopeActive($query) + { + return $query->where('active', true); + } + + /** + * Scope a query to only include root categories. + */ + public function scopeRoots($query) + { + return $query->whereNull('parent_id'); + } + + /** + * Check if the category has children. + */ + public function hasChildren(): bool + { + return $this->children()->exists(); + } + + /** + * Check if the category has products. + * Note: Assuming a Product model exists with a category_id or similar relationship. + * Since the migration for products might not be linked yet, this is a placeholder or checks a relation if defined. + * For now, I will assume a products relationship exists or will be added. + */ + public function products(): HasMany + { + return $this->hasMany(Product::class, 'categorie_id'); + } + + public function hasProducts(): bool + { + return $this->products()->exists(); + } +} diff --git a/thanasoft-back/app/Models/PurchaseOrder.php b/thanasoft-back/app/Models/PurchaseOrder.php new file mode 100644 index 0000000..28598cc --- /dev/null +++ b/thanasoft-back/app/Models/PurchaseOrder.php @@ -0,0 +1,77 @@ +po_number)) { + $prefix = 'CMD-' . now()->format('Ym') . '-'; + $lastOrder = self::where('po_number', 'like', $prefix . '%') + ->orderBy('po_number', 'desc') + ->first(); + + if ($lastOrder) { + $lastNumber = intval(substr($lastOrder->po_number, -4)); + $newNumber = $lastNumber + 1; + } else { + $newNumber = 1; + } + + $purchaseOrder->po_number = $prefix . str_pad((string) $newNumber, 4, '0', STR_PAD_LEFT); + } + }); + } + + protected $fillable = [ + 'fournisseur_id', + 'po_number', + 'status', + 'order_date', + 'expected_date', + 'currency', + 'total_ht', + 'total_tva', + 'total_ttc', + 'notes', + 'delivery_address', + ]; + + protected $casts = [ + 'order_date' => 'date', + 'expected_date' => 'date', + 'total_ht' => 'decimal:2', + 'total_tva' => 'decimal:2', + 'total_ttc' => 'decimal:2', + ]; + + public function fournisseur(): BelongsTo + { + return $this->belongsTo(Fournisseur::class); + } + + public function lines(): HasMany + { + return $this->hasMany(PurchaseOrderLine::class); + } + + /** + * Get the goods receipts for this purchase order. + */ + public function goodsReceipts(): HasMany + { + return $this->hasMany(GoodsReceipt::class); + } +} diff --git a/thanasoft-back/app/Models/PurchaseOrderLine.php b/thanasoft-back/app/Models/PurchaseOrderLine.php new file mode 100644 index 0000000..922f2b3 --- /dev/null +++ b/thanasoft-back/app/Models/PurchaseOrderLine.php @@ -0,0 +1,43 @@ + 'decimal:3', + 'unit_price' => 'decimal:2', + 'tva_rate' => 'decimal:2', + 'discount_pct' => 'decimal:2', + 'total_ht' => 'decimal:2', + ]; + + public function purchaseOrder(): BelongsTo + { + return $this->belongsTo(PurchaseOrder::class); + } + + public function product(): BelongsTo + { + return $this->belongsTo(Product::class); + } +} diff --git a/thanasoft-back/app/Models/Quote.php b/thanasoft-back/app/Models/Quote.php new file mode 100644 index 0000000..c010ee2 --- /dev/null +++ b/thanasoft-back/app/Models/Quote.php @@ -0,0 +1,78 @@ +format('Ym') . '-'; + $lastQuote = self::where('reference', 'like', $prefix . '%') + ->orderBy('reference', 'desc') + ->first(); + + if ($lastQuote) { + $lastNumber = intval(substr($lastQuote->reference, -4)); + $newNumber = $lastNumber + 1; + } else { + $newNumber = 1; + } + + $quote->reference = $prefix . str_pad((string)$newNumber, 4, '0', STR_PAD_LEFT); + }); + } + + protected $casts = [ + 'quote_date' => 'date', + 'valid_until' => 'date', + 'total_ht' => 'decimal:2', + 'total_tva' => 'decimal:2', + 'total_ttc' => 'decimal:2', + ]; + + public function client() + { + return $this->belongsTo(Client::class); + } + + public function group() + { + return $this->belongsTo(ClientGroup::class); + } + + public function lines() + { + return $this->hasMany(QuoteLine::class); + } + + public function history() + { + return $this->hasMany(DocumentStatusHistory::class, 'document_id') + ->where('document_type', 'quote') + ->orderBy('changed_at', 'desc'); + } + + public function interventions() + { + return $this->hasMany(Intervention::class); + } +} diff --git a/thanasoft-back/app/Models/QuoteLine.php b/thanasoft-back/app/Models/QuoteLine.php new file mode 100644 index 0000000..22ecd20 --- /dev/null +++ b/thanasoft-back/app/Models/QuoteLine.php @@ -0,0 +1,48 @@ +belongsTo(Quote::class); + } + + public function product() + { + return $this->belongsTo(Product::class); + } + + public function packaging() + { + // Assuming ProductPackaging model exists + return $this->belongsTo(\App\Models\Stock\ProductPackaging::class, 'packaging_id'); + } + + public function tvaRate() + { + // Assuming TvaRate model exists + return $this->belongsTo(\App\Models\TvaRate::class, 'tva_rate_id'); + } +} diff --git a/thanasoft-back/app/Models/Stock/ProductPackaging.php b/thanasoft-back/app/Models/Stock/ProductPackaging.php new file mode 100644 index 0000000..0eac92f --- /dev/null +++ b/thanasoft-back/app/Models/Stock/ProductPackaging.php @@ -0,0 +1,30 @@ + 'decimal:3', + ]; + + /** + * Get the product that owns the packaging. + */ + public function product(): BelongsTo + { + return $this->belongsTo(Product::class); + } +} diff --git a/thanasoft-back/app/Models/StockItem.php b/thanasoft-back/app/Models/StockItem.php new file mode 100644 index 0000000..e16624f --- /dev/null +++ b/thanasoft-back/app/Models/StockItem.php @@ -0,0 +1,39 @@ + 'decimal:3', + 'safety_stock_base' => 'decimal:3', + ]; + + /** + * Get the product associated with this stock item. + */ + public function product(): BelongsTo + { + return $this->belongsTo(Product::class); + } + + /** + * Get the warehouse where this stock item is located. + */ + public function warehouse(): BelongsTo + { + return $this->belongsTo(Warehouse::class); + } +} diff --git a/thanasoft-back/app/Models/StockMove.php b/thanasoft-back/app/Models/StockMove.php new file mode 100644 index 0000000..937a5c2 --- /dev/null +++ b/thanasoft-back/app/Models/StockMove.php @@ -0,0 +1,65 @@ + 'decimal:3', + 'units_qty' => 'decimal:3', + 'qty_base' => 'decimal:3', + 'moved_at' => 'datetime', + ]; + + /** + * Get the product being moved. + */ + public function product(): BelongsTo + { + return $this->belongsTo(Product::class); + } + + /** + * Get the source warehouse. + */ + public function fromWarehouse(): BelongsTo + { + return $this->belongsTo(Warehouse::class, 'from_warehouse_id'); + } + + /** + * Get the destination warehouse. + */ + public function toWarehouse(): BelongsTo + { + return $this->belongsTo(Warehouse::class, 'to_warehouse_id'); + } + + /** + * Get the packaging used for this move. + */ + public function packaging(): BelongsTo + { + return $this->belongsTo(ProductPackaging::class, 'packaging_id'); + } +} diff --git a/thanasoft-back/app/Models/Thanatopractitioner.php b/thanasoft-back/app/Models/Thanatopractitioner.php new file mode 100644 index 0000000..80e8267 --- /dev/null +++ b/thanasoft-back/app/Models/Thanatopractitioner.php @@ -0,0 +1,118 @@ + + */ + protected $fillable = [ + 'employee_id', + 'diploma_number', + 'diploma_date', + 'authorization_number', + 'authorization_issue_date', + 'authorization_expiry_date', + 'notes', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'diploma_date' => 'date', + 'authorization_issue_date' => 'date', + 'authorization_expiry_date' => 'date', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * Get the employee that owns the thanatopractitioner. + */ + public function employee(): BelongsTo + { + return $this->belongsTo(Employee::class); + } + + /** + * Get all documents associated with the thanatopractitioner. + */ + public function documents(): HasMany + { + return $this->hasMany(PractitionerDocument::class, 'practitioner_id'); + } + + /** + * Get the interventions assigned to the thanatopractitioner. + */ + public function interventions(): BelongsToMany + { + return $this->belongsToMany(Intervention::class, 'intervention_practitioner') + ->withPivot('role', 'assigned_at') + ->withTimestamps(); + } + + /** + * Get the interventions where this practitioner is the principal. + */ + public function principalInterventions(): BelongsToMany + { + return $this->belongsToMany(Intervention::class, 'intervention_practitioner') + ->wherePivot('role', 'principal') + ->withPivot('role', 'assigned_at') + ->withTimestamps(); + } + + /** + * Get the interventions where this practitioner is an assistant. + */ + public function assistantInterventions(): BelongsToMany + { + return $this->belongsToMany(Intervention::class, 'intervention_practitioner') + ->wherePivot('role', 'assistant') + ->withPivot('role', 'assigned_at') + ->withTimestamps(); + } + + /** + * Scope a query to only include practitioners with valid authorization. + */ + public function scopeWithValidAuthorization($query) + { + return $query->where('authorization_expiry_date', '>=', now()); + } + + /** + * Scope a query to only include practitioners with expired authorization. + */ + public function scopeWithExpiredAuthorization($query) + { + return $query->where('authorization_expiry_date', '<', now()); + } + + /** + * Check if the authorization is still valid. + */ + public function getIsAuthorizationValidAttribute(): bool + { + if (!$this->authorization_expiry_date) { + return false; + } + + return $this->authorization_expiry_date >= now(); + } +} diff --git a/thanasoft-back/app/Models/TvaRate.php b/thanasoft-back/app/Models/TvaRate.php new file mode 100644 index 0000000..14c850b --- /dev/null +++ b/thanasoft-back/app/Models/TvaRate.php @@ -0,0 +1,20 @@ + 'decimal:2', + ]; +} diff --git a/thanasoft-back/app/Models/User.php b/thanasoft-back/app/Models/User.php index 91135d7..86f671a 100644 --- a/thanasoft-back/app/Models/User.php +++ b/thanasoft-back/app/Models/User.php @@ -4,14 +4,18 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; +use Spatie\Permission\Traits\HasRoles; class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasApiTokens, HasFactory, Notifiable; + use HasApiTokens, HasFactory, HasRoles, Notifiable; + + protected string $guard_name = 'sanctum'; /** * The attributes that are mass assignable. @@ -46,4 +50,19 @@ class User extends Authenticatable 'password' => 'hashed', ]; } + + public function canViewAgenda(): bool + { + return $this->can('employee_agenda.view'); + } + + public function employee(): HasOne + { + return $this->hasOne(Employee::class); + } + + public function mailboxSetting(): HasOne + { + return $this->hasOne(UserMailboxSetting::class); + } } diff --git a/thanasoft-back/app/Models/UserMailboxSetting.php b/thanasoft-back/app/Models/UserMailboxSetting.php new file mode 100644 index 0000000..4159aad --- /dev/null +++ b/thanasoft-back/app/Models/UserMailboxSetting.php @@ -0,0 +1,70 @@ + + */ + protected $fillable = [ + 'user_id', + 'imap_host', + 'imap_port', + 'imap_encryption', + 'imap_validate_cert', + 'imap_username', + 'imap_password', + 'imap_folder', + 'smtp_host', + 'smtp_port', + 'smtp_encryption', + 'smtp_validate_cert', + 'smtp_username', + 'smtp_password', + 'smtp_from_address', + 'smtp_from_name', + 'last_synced_at', + 'last_sync_error', + ]; + + /** + * @var array + */ + protected $casts = [ + 'imap_validate_cert' => 'boolean', + 'smtp_validate_cert' => 'boolean', + 'imap_password' => 'encrypted', + 'smtp_password' => 'encrypted', + 'last_synced_at' => 'datetime', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function hasImapConfiguration(): bool + { + return filled($this->imap_host) + && filled($this->imap_port) + && filled($this->imap_username) + && filled($this->imap_password); + } + + public function hasSmtpConfiguration(): bool + { + return filled($this->smtp_host) + && filled($this->smtp_port) + && filled($this->smtp_username) + && filled($this->smtp_password); + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Models/Vehicle.php b/thanasoft-back/app/Models/Vehicle.php new file mode 100644 index 0000000..bbe7c0e --- /dev/null +++ b/thanasoft-back/app/Models/Vehicle.php @@ -0,0 +1,44 @@ + 'integer', + 'year' => 'integer', + ]; + + public function primaryUser(): BelongsTo + { + return $this->belongsTo(Employee::class, 'primary_user_id'); + } + + public function convoys(): HasMany + { + return $this->hasMany(Convoy::class); + } +} diff --git a/thanasoft-back/app/Models/Warehouse.php b/thanasoft-back/app/Models/Warehouse.php new file mode 100644 index 0000000..1cc8479 --- /dev/null +++ b/thanasoft-back/app/Models/Warehouse.php @@ -0,0 +1,52 @@ +hasMany(StockItem::class); + } + + /** + * Get the stock moves from this warehouse. + */ + public function movesFrom(): HasMany + { + return $this->hasMany(StockMove::class, 'from_warehouse_id'); + } + + /** + * Get the stock moves to this warehouse. + */ + public function movesTo(): HasMany + { + return $this->hasMany(StockMove::class, 'to_warehouse_id'); + } + + /** + * Get the goods receipts for this warehouse. + */ + public function goodsReceipts(): HasMany + { + return $this->hasMany(GoodsReceipt::class); + } +} diff --git a/thanasoft-back/app/Models/WebmailMessage.php b/thanasoft-back/app/Models/WebmailMessage.php new file mode 100644 index 0000000..f9b6712 --- /dev/null +++ b/thanasoft-back/app/Models/WebmailMessage.php @@ -0,0 +1,59 @@ + + */ + protected $fillable = [ + 'user_id', + 'message_uid', + 'direction', + 'folder', + 'status', + 'from_email', + 'from_name', + 'to_recipients', + 'cc_recipients', + 'bcc_recipients', + 'subject', + 'body', + 'snippet', + 'attachments', + 'metadata', + 'read_at', + 'starred_at', + 'sent_at', + 'received_at', + ]; + + /** + * @var array + */ + protected $casts = [ + 'to_recipients' => 'array', + 'cc_recipients' => 'array', + 'bcc_recipients' => 'array', + 'attachments' => 'array', + 'metadata' => 'array', + 'read_at' => 'datetime', + 'starred_at' => 'datetime', + 'sent_at' => 'datetime', + 'received_at' => 'datetime', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Providers/AppServiceProvider.php b/thanasoft-back/app/Providers/AppServiceProvider.php index 452e6b6..53eb5c6 100644 --- a/thanasoft-back/app/Providers/AppServiceProvider.php +++ b/thanasoft-back/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use Illuminate\Support\Facades\Schema; class AppServiceProvider extends ServiceProvider { @@ -11,14 +12,121 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + // Repository interface to implementation bindings + $this->app->bind(\App\Repositories\ClientRepositoryInterface::class, function ($app) { + return new \App\Repositories\ClientRepository( + $app->make(\App\Models\Client::class), + $app->make(\App\Repositories\ClientActivityTimelineRepositoryInterface::class) + ); + }); + + $this->app->bind(\App\Repositories\ClientGroupRepositoryInterface::class, function ($app) { + return new \App\Repositories\ClientGroupRepository($app->make(\App\Models\ClientGroup::class)); + }); + + $this->app->bind(\App\Repositories\PriceListRepositoryInterface::class, function ($app) { + return new \App\Repositories\PriceListRepository($app->make(\App\Models\PriceList::class)); + }); + + $this->app->bind(\App\Repositories\UserRepositoryInterface::class, function ($app) { + return new \App\Repositories\UserRepository($app->make(\App\Models\User::class)); + }); + + $this->app->bind(\App\Repositories\ClientContactRepositoryInterface::class, function ($app) { + return new \App\Repositories\ClientContactRepository($app->make(\App\Models\ClientContact::class)); + }); + + $this->app->bind(\App\Repositories\ContactRepositoryInterface::class, function ($app) { + return new \App\Repositories\ContactRepository($app->make(\App\Models\Contact::class)); + }); + + $this->app->bind(\App\Repositories\ClientLocationRepositoryInterface::class, function ($app) { + return new \App\Repositories\ClientLocationRepository($app->make(\App\Models\ClientLocation::class)); + }); + + $this->app->bind(\App\Repositories\FournisseurRepositoryInterface::class, function ($app) { + return new \App\Repositories\FournisseurRepository($app->make(\App\Models\Fournisseur::class)); + }); + + $this->app->bind(\App\Repositories\ProductRepositoryInterface::class, function ($app) { + return new \App\Repositories\ProductRepository($app->make(\App\Models\Product::class)); + }); + + $this->app->bind(\App\Repositories\ProductCategoryRepositoryInterface::class, function ($app) { + return new \App\Repositories\ProductCategoryRepository($app->make(\App\Models\ProductCategory::class)); + }); + + // Employee management repository bindings + $this->app->bind(\App\Repositories\EmployeeRepositoryInterface::class, function ($app) { + return new \App\Repositories\EmployeeRepository($app->make(\App\Models\Employee::class)); + }); + + $this->app->bind(\App\Repositories\ThanatopractitionerRepositoryInterface::class, function ($app) { + return new \App\Repositories\ThanatopractitionerRepository($app->make(\App\Models\Thanatopractitioner::class)); + }); + + $this->app->bind(\App\Repositories\PractitionerDocumentRepositoryInterface::class, function ($app) { + return new \App\Repositories\PractitionerDocumentRepository($app->make(\App\Models\PractitionerDocument::class)); + }); + + $this->app->bind(\App\Repositories\InterventionRepositoryInterface::class, \App\Repositories\InterventionRepository::class); + + $this->app->bind(\App\Repositories\DeceasedRepositoryInterface::class, \App\Repositories\DeceasedRepository::class); + + $this->app->bind(\App\Repositories\InterventionPractitionerRepositoryInterface::class, \App\Repositories\InterventionPractitionerRepository::class); + + $this->app->bind(\App\Repositories\FileRepositoryInterface::class, \App\Repositories\FileRepository::class); + + $this->app->bind(\App\Repositories\ProductCategoryRepositoryInterface::class, \App\Repositories\ProductCategoryRepository::class); + + $this->app->bind(\App\Repositories\InvoiceRepositoryInterface::class, function ($app) { + return new \App\Repositories\InvoiceRepository( + $app->make(\App\Models\Invoice::class), + $app->make(\App\Repositories\InvoiceLineRepositoryInterface::class), + $app->make(\App\Repositories\ClientActivityTimelineRepositoryInterface::class) + ); + }); + + $this->app->bind(\App\Repositories\InvoiceLineRepositoryInterface::class, \App\Repositories\InvoiceLineRepository::class); + + $this->app->bind(\App\Repositories\AvoirRepositoryInterface::class, function ($app) { + return new \App\Repositories\AvoirRepository( + $app->make(\App\Models\Avoir::class), + $app->make(\App\Repositories\AvoirLineRepositoryInterface::class), + $app->make(\App\Repositories\ClientActivityTimelineRepositoryInterface::class) + ); + }); + + $this->app->bind(\App\Repositories\AvoirLineRepositoryInterface::class, \App\Repositories\AvoirLineRepository::class); + + $this->app->bind(\App\Repositories\QuoteRepositoryInterface::class, function ($app) { + return new \App\Repositories\QuoteRepository( + $app->make(\App\Models\Quote::class), + $app->make(\App\Repositories\QuoteLineRepositoryInterface::class), + $app->make(\App\Repositories\InvoiceRepositoryInterface::class), + $app->make(\App\Repositories\ClientActivityTimelineRepositoryInterface::class) + ); + }); + + $this->app->bind(\App\Repositories\QuoteLineRepositoryInterface::class, \App\Repositories\QuoteLineRepository::class); + + $this->app->bind(\App\Repositories\QuoteLineRepositoryInterface::class, \App\Repositories\QuoteLineRepository::class); + + $this->app->bind(\App\Repositories\ClientActivityTimelineRepositoryInterface::class, \App\Repositories\ClientActivityTimelineRepository::class); + + $this->app->bind(\App\Repositories\PurchaseOrderRepositoryInterface::class, \App\Repositories\PurchaseOrderRepository::class); + $this->app->bind(\App\Repositories\DeceasedDocumentRepositoryInterface::class, \App\Repositories\DeceasedDocumentRepository::class); + $this->app->bind(\App\Repositories\VehicleRepositoryInterface::class, \App\Repositories\VehicleRepository::class); + $this->app->bind(\App\Repositories\ConvoyRepositoryInterface::class, \App\Repositories\ConvoyRepository::class); } + + /** * Bootstrap any application services. */ public function boot(): void { - // + Schema::defaultStringLength(191); } } diff --git a/thanasoft-back/app/Providers/RepositoryServiceProvider.php b/thanasoft-back/app/Providers/RepositoryServiceProvider.php new file mode 100644 index 0000000..129bc49 --- /dev/null +++ b/thanasoft-back/app/Providers/RepositoryServiceProvider.php @@ -0,0 +1,48 @@ +app->bind(AccessControlRepositoryInterface::class, AccessControlRepository::class); + $this->app->bind(DeceasedRepositoryInterface::class, DeceasedRepository::class); + $this->app->bind(DeceasedDocumentRepositoryInterface::class, DeceasedDocumentRepository::class); + $this->app->bind(InterventionRepositoryInterface::class, InterventionRepository::class); + $this->app->bind(FileRepositoryInterface::class, FileRepository::class); + $this->app->bind(WebmailMessageRepositoryInterface::class, WebmailMessageRepository::class); + $this->app->bind(\App\Repositories\PurchaseOrderRepositoryInterface::class, \App\Repositories\PurchaseOrderRepository::class); + $this->app->bind(\App\Repositories\WarehouseRepositoryInterface::class, \App\Repositories\WarehouseRepository::class); + $this->app->bind(\App\Repositories\StockItemRepositoryInterface::class, \App\Repositories\StockItemRepository::class); + $this->app->bind(\App\Repositories\StockMoveRepositoryInterface::class, \App\Repositories\StockMoveRepository::class); + $this->app->bind(\App\Repositories\ProductPackagingRepositoryInterface::class, \App\Repositories\ProductPackagingRepository::class); + $this->app->bind(\App\Repositories\TvaRateRepositoryInterface::class, \App\Repositories\TvaRateRepository::class); + $this->app->bind(\App\Repositories\GoodsReceiptRepositoryInterface::class, \App\Repositories\GoodsReceiptRepository::class); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + // + } +} diff --git a/thanasoft-back/app/Repositories/AccessControlRepository.php b/thanasoft-back/app/Repositories/AccessControlRepository.php new file mode 100644 index 0000000..0695cd9 --- /dev/null +++ b/thanasoft-back/app/Repositories/AccessControlRepository.php @@ -0,0 +1,146 @@ + Role::query() + ->with('permissions:id,name') + ->withCount('users') + ->orderBy('name') + ->get(), + 'permissions' => Permission::query() + ->with('roles:id,name') + ->orderBy('name') + ->get(), + ]; + } + + public function createRole(array $attributes): Role + { + return DB::transaction(function () use ($attributes): Role { + $permissions = $attributes['permissions'] ?? []; + unset($attributes['permissions']); + + $role = Role::query()->create([ + 'name' => $attributes['name'], + 'guard_name' => $attributes['guard_name'] ?? 'sanctum', + ]); + + if (is_array($permissions) && $permissions !== []) { + $role->syncPermissions($permissions); + } + + return $role->load('permissions:id,name'); + }); + } + + public function updateRole(int $id, array $attributes): ?Role + { + return DB::transaction(function () use ($id, $attributes): ?Role { + $role = Role::query()->find($id); + + if (! $role) { + return null; + } + + $permissions = $attributes['permissions'] ?? null; + unset($attributes['permissions']); + + if (array_key_exists('name', $attributes)) { + $role->name = $attributes['name']; + } + + if (array_key_exists('guard_name', $attributes) && is_string($attributes['guard_name'])) { + $role->guard_name = $attributes['guard_name']; + } + + $role->save(); + + if (is_array($permissions)) { + $role->syncPermissions($permissions); + } + + return $role->load('permissions:id,name'); + }); + } + + public function deleteRole(int $id): bool + { + return (bool) DB::transaction(function () use ($id): bool { + $role = Role::query()->find($id); + + if (! $role) { + return false; + } + + $role->delete(); + + return true; + }); + } + + public function syncRolePermissions(int $id, array $permissions): ?Role + { + return DB::transaction(function () use ($id, $permissions): ?Role { + $role = Role::query()->find($id); + + if (! $role) { + return null; + } + + $role->syncPermissions($permissions); + + return $role->load('permissions:id,name'); + }); + } + + public function createPermission(array $attributes): Permission + { + return Permission::query()->create([ + 'name' => $attributes['name'], + 'guard_name' => $attributes['guard_name'] ?? 'sanctum', + ]); + } + + public function updatePermission(int $id, array $attributes): ?Permission + { + $permission = Permission::query()->find($id); + + if (! $permission) { + return null; + } + + if (array_key_exists('name', $attributes)) { + $permission->name = $attributes['name']; + } + + if (array_key_exists('guard_name', $attributes) && is_string($attributes['guard_name'])) { + $permission->guard_name = $attributes['guard_name']; + } + + $permission->save(); + + return $permission->load('roles:id,name'); + } + + public function deletePermission(int $id): bool + { + $permission = Permission::query()->find($id); + + if (! $permission) { + return false; + } + + return (bool) $permission->delete(); + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Repositories/AccessControlRepositoryInterface.php b/thanasoft-back/app/Repositories/AccessControlRepositoryInterface.php new file mode 100644 index 0000000..c9d09b7 --- /dev/null +++ b/thanasoft-back/app/Repositories/AccessControlRepositoryInterface.php @@ -0,0 +1,45 @@ +, permissions: \Illuminate\Support\Collection} + */ + public function index(): array; + + /** + * @param array $attributes + */ + public function createRole(array $attributes): Role; + + /** + * @param array $attributes + */ + public function updateRole(int $id, array $attributes): ?Role; + + public function deleteRole(int $id): bool; + + /** + * @param array $permissions + */ + public function syncRolePermissions(int $id, array $permissions): ?Role; + + /** + * @param array $attributes + */ + public function createPermission(array $attributes): Permission; + + /** + * @param array $attributes + */ + public function updatePermission(int $id, array $attributes): ?Permission; + + public function deletePermission(int $id): bool; +} \ No newline at end of file diff --git a/thanasoft-back/app/Repositories/AvoirLineRepository.php b/thanasoft-back/app/Repositories/AvoirLineRepository.php new file mode 100644 index 0000000..4014105 --- /dev/null +++ b/thanasoft-back/app/Repositories/AvoirLineRepository.php @@ -0,0 +1,15 @@ +model->with(['client', 'lines.product'])->get($columns); + } + + public function create(array $data): Avoir + { + return DB::transaction(function () use ($data) { + try { + // Create the avoir + $avoir = parent::create($data); + + // Create the avoir lines + if (isset($data['lines']) && is_array($data['lines'])) { + foreach ($data['lines'] as $lineData) { + $lineData['avoir_id'] = $avoir->id; + $this->avoirLineRepository->create($lineData); + } + } + + // Record initial status history + $this->recordHistory((int)$avoir->id, null, $avoir->status, 'Avoir created'); + + try { + $this->timelineRepository->logActivity([ + 'client_id' => $avoir->client_id, + 'actor_type' => 'user', + 'actor_user_id' => auth()->id(), + 'event_type' => 'avoir_created', + 'entity_type' => 'avoir', + 'entity_id' => $avoir->id, + 'title' => 'Nouvel avoir créé', + 'description' => "L'avoir #{$avoir->avoir_number} a été créé.", + 'created_at' => now(), + ]); + } catch (\Exception $e) { + Log::error("Failed to log avoir creation activity: " . $e->getMessage()); + } + + return $avoir; + } catch (\Exception $e) { + Log::error('Error creating avoir with lines: ' . $e->getMessage(), [ + 'exception' => $e, + 'data' => $data, + ]); + throw $e; + } + }); + } + + public function update(int|string $id, array $attributes): bool + { + return DB::transaction(function () use ($id, $attributes) { + try { + $avoir = $this->find($id); + if (!$avoir) { + return false; + } + + $oldStatus = $avoir->status; + + // Update the avoir + $updated = parent::update($id, $attributes); + + if ($updated) { + $newStatus = $attributes['status'] ?? $oldStatus; + + // If status changed, record history + if ($oldStatus !== $newStatus) { + $this->recordHistory((int) $id, $oldStatus, $newStatus, 'Avoir status updated'); + } + } + + return $updated; + } catch (\Exception $e) { + Log::error('Error updating avoir: ' . $e->getMessage(), [ + 'id' => $id, + 'attributes' => $attributes, + 'exception' => $e, + ]); + throw $e; + } + }); + } + + public function find(int|string $id, array $columns = ['*']): ?Avoir + { + return $this->model->with(['client', 'lines.product', 'history.user'])->find($id, $columns); + } + + private function recordHistory(int $avoirId, ?string $oldStatus, string $newStatus, ?string $comment = null): void + { + \App\Models\DocumentStatusHistory::create([ + 'document_type' => 'avoir', + 'document_id' => $avoirId, + 'old_status' => $oldStatus, + 'new_status' => $newStatus, + 'changed_by' => auth()->id(), + 'comment' => $comment, + 'changed_at' => now(), + ]); + } +} diff --git a/thanasoft-back/app/Repositories/AvoirRepositoryInterface.php b/thanasoft-back/app/Repositories/AvoirRepositoryInterface.php new file mode 100644 index 0000000..df45f61 --- /dev/null +++ b/thanasoft-back/app/Repositories/AvoirRepositoryInterface.php @@ -0,0 +1,9 @@ + $columns + * @return Collection + */ + public function all(array $columns = ['*']): Collection + { + return $this->model->newQuery()->get($columns); + } + + /** + * @param int|string $id + * @param array $columns + */ + public function find(int|string $id, array $columns = ['*']): ?Model + { + return $this->model->newQuery()->find($id, $columns); + } + + /** + * Create a new model instance with transaction support. + * + * @param array $attributes + * @throws Exception + */ + public function create(array $attributes): Model + { + try { + DB::beginTransaction(); + + // Uses mass assignment; ensure $fillable is set on the model + $model = $this->model->newQuery()->create($attributes); + + DB::commit(); + + return $model; + } catch (Exception $e) { + DB::rollBack(); + Log::error('Error creating ' . get_class($this->model) . ': ' . $e->getMessage(), [ + 'attributes' => $attributes, + 'exception' => $e + ]); + throw $e; + } + } + + /** + * Update an existing model instance with transaction support. + * + * @param int|string $id + * @param array $attributes + * @throws Exception + */ + public function update(int|string $id, array $attributes): bool + { + try { + DB::beginTransaction(); + + $instance = $this->find($id); + if (! $instance) { + DB::rollBack(); + return false; + } + + $result = $instance->fill($attributes)->save(); + + DB::commit(); + + return $result; + } catch (Exception $e) { + DB::rollBack(); + Log::error('Error updating ' . get_class($this->model) . ' with ID ' . $id . ': ' . $e->getMessage(), [ + 'id' => $id, + 'attributes' => $attributes, + 'exception' => $e + ]); + throw $e; + } + } + + /** + * Delete a model instance with transaction support. + * + * @param int|string $id + * @throws Exception + */ + public function delete(int|string $id): bool + { + try { + DB::beginTransaction(); + + $instance = $this->find($id); + if (! $instance) { + DB::rollBack(); + return false; + } + + $result = (bool) $instance->delete(); + + DB::commit(); + + return $result; + } catch (Exception $e) { + DB::rollBack(); + Log::error('Error deleting ' . get_class($this->model) . ' with ID ' . $id . ': ' . $e->getMessage(), [ + 'id' => $id, + 'exception' => $e + ]); + throw $e; + } + } +} diff --git a/thanasoft-back/app/Repositories/BaseRepositoryInterface.php b/thanasoft-back/app/Repositories/BaseRepositoryInterface.php new file mode 100644 index 0000000..ea8688d --- /dev/null +++ b/thanasoft-back/app/Repositories/BaseRepositoryInterface.php @@ -0,0 +1,52 @@ + $columns + * @return Collection + */ + public function all(array $columns = ['*']): Collection; + + /** + * Find a record by its primary key. + * + * @param int|string $id + * @param array $columns + */ + public function find(int|string $id, array $columns = ['*']): ?Model; + + /** + * Create a new record with the given attributes. + * + * @param array $attributes + */ + public function create(array $attributes): Model; + + /** + * Update an existing record by id with the given attributes. + * + * @param int|string $id + * @param array $attributes + */ + public function update(int|string $id, array $attributes): bool; + + /** + * Delete a record by its primary key. + * + * @param int|string $id + */ + public function delete(int|string $id): bool; +} diff --git a/thanasoft-back/app/Repositories/ClientActivityTimelineRepository.php b/thanasoft-back/app/Repositories/ClientActivityTimelineRepository.php new file mode 100644 index 0000000..5e99b42 --- /dev/null +++ b/thanasoft-back/app/Repositories/ClientActivityTimelineRepository.php @@ -0,0 +1,43 @@ +model + ->where('client_id', $clientId) + ->with('actor') // Load actor relationship + ->orderBy('created_at', 'desc') + ->paginate($perPage); + } + + /** + * Log a new activity + * + * @param array $data + * @return ClientActivityTimeline + */ + public function logActivity(array $data) + { + return $this->create($data); + } +} diff --git a/thanasoft-back/app/Repositories/ClientActivityTimelineRepositoryInterface.php b/thanasoft-back/app/Repositories/ClientActivityTimelineRepositoryInterface.php new file mode 100644 index 0000000..0e060f8 --- /dev/null +++ b/thanasoft-back/app/Repositories/ClientActivityTimelineRepositoryInterface.php @@ -0,0 +1,27 @@ +model->newQuery()->withCount('clients'); + + if (!empty($filters['search'])) { + $query->where(function ($builder) use ($filters) { + $builder + ->where('name', 'like', '%' . $filters['search'] . '%') + ->orWhere('description', 'like', '%' . $filters['search'] . '%'); + }); + } + + $sortField = $filters['sort_by'] ?? 'created_at'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + + return $query->orderBy($sortField, $sortDirection)->paginate($perPage); + } +} diff --git a/thanasoft-back/app/Repositories/ClientGroupRepositoryInterface.php b/thanasoft-back/app/Repositories/ClientGroupRepositoryInterface.php new file mode 100644 index 0000000..8cd3303 --- /dev/null +++ b/thanasoft-back/app/Repositories/ClientGroupRepositoryInterface.php @@ -0,0 +1,12 @@ +model->newQuery(); + $query->where('client_id', $client_id); + return $query->get(); + } + + /** + * Get paginated client locations with optional filters + */ + public function getPaginated(array $filters = [], int $perPage = 10) + { + $query = $this->model->newQuery(); + + // Filter by client_id + if (isset($filters['client_id'])) { + $query->where('client_id', $filters['client_id']); + } + + // Filter by is_default + if (isset($filters['is_default'])) { + $query->where('is_default', $filters['is_default']); + } + + // Search filter + if (!empty($filters['search'])) { + $search = $filters['search']; + $query->where(function ($q) use ($search) { + $q->where('name', 'LIKE', "%{$search}%") + ->orWhere('address_line1', 'LIKE', "%{$search}%") + ->orWhere('address_line2', 'LIKE', "%{$search}%") + ->orWhere('city', 'LIKE', "%{$search}%") + ->orWhere('postal_code', 'LIKE', "%{$search}%"); + }); + } + + // Order by + $query->orderBy('is_default', 'desc') + ->orderBy('created_at', 'desc'); + + return $query->paginate($perPage); + } + + /** + * Get paginated locations for a specific client + */ + public function getPaginatedByClientId(int $clientId, array $filters = [], int $perPage = 10): LengthAwarePaginator + { + $query = $this->model->newQuery()->where('client_id', $clientId); + + if (isset($filters['is_default'])) { + $query->where('is_default', $filters['is_default']); + } + + if (!empty($filters['search'])) { + $search = $filters['search']; + $query->where(function ($q) use ($search) { + $q->where('name', 'LIKE', "%{$search}%") + ->orWhere('address_line1', 'LIKE', "%{$search}%") + ->orWhere('address_line2', 'LIKE', "%{$search}%") + ->orWhere('city', 'LIKE', "%{$search}%") + ->orWhere('postal_code', 'LIKE', "%{$search}%"); + }); + } + + $query->orderBy('is_default', 'desc') + ->orderBy('created_at', 'desc'); + + return $query->paginate($perPage); + } + + /** + * Get default location for a client + */ + public function getDefaultByClientId(int $clientId): ?ClientLocation + { + return $this->model->where('client_id', $clientId) + ->where('is_default', true) + ->first(); + } + + /** + * Set a location as default and update others + */ + public function setAsDefault(int $locationId): ClientLocation + { + $location = $this->find($locationId); + + $this->model->where('client_id', $location->client_id) + ->where('id', '!=', $locationId) + ->update(['is_default' => false]); + + // Set this location as default + $location->update(['is_default' => true]); + + return $location->fresh(); + } +} diff --git a/thanasoft-back/app/Repositories/ClientLocationRepositoryInterface.php b/thanasoft-back/app/Repositories/ClientLocationRepositoryInterface.php new file mode 100644 index 0000000..d858a21 --- /dev/null +++ b/thanasoft-back/app/Repositories/ClientLocationRepositoryInterface.php @@ -0,0 +1,20 @@ +timelineRepository->logActivity([ + 'client_id' => $client->id, + 'actor_type' => 'user', + 'actor_user_id' => auth()->id(), + 'event_type' => 'client_created', + 'entity_type' => 'client', + 'entity_id' => $client->id, + 'title' => 'Nouveau client créé', + 'description' => "Le client {$client->name} a été ajouté au système.", + 'created_at' => now(), + ]); + } catch (\Exception $e) { + LaravelLog::error("Failed to log client creation activity: " . $e->getMessage()); + } + + return $client; + } + + + /** + * Get paginated clients + */ + public function paginate(int $perPage = 15, array $filters = []): LengthAwarePaginator + { + $query = $this->model->newQuery(); + + // Apply filters + if (!empty($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('name', 'like', '%' . $filters['search'] . '%') + ->orWhere('email', 'like', '%' . $filters['search'] . '%') + ->orWhere('vat_number', 'like', '%' . $filters['search'] . '%') + ->orWhere('siret', 'like', '%' . $filters['search'] . '%'); + }); + } + + if (isset($filters['is_active'])) { + $query->where('is_active', $filters['is_active']); + } + + if (!empty($filters['group_id'])) { + $query->where('group_id', $filters['group_id']); + } + + if (!empty($filters['client_category_id'])) { + $query->where('client_category_id', $filters['client_category_id']); + } + + // Apply sorting + $sortField = $filters['sort_by'] ?? 'created_at'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + $query->orderBy($sortField, $sortDirection); + + return $query->paginate($perPage); + } + + + public function searchByName(string $name, int $perPage = 15, bool $exactMatch = false) + { + $query = $this->model->newQuery(); + + if ($exactMatch) { + $query->where('name', $name); + } else { + $query->where('name', 'like', '%' . $name . '%'); + } + + return $query->get(); + } +} diff --git a/thanasoft-back/app/Repositories/ClientRepositoryInterface.php b/thanasoft-back/app/Repositories/ClientRepositoryInterface.php new file mode 100644 index 0000000..9de1baa --- /dev/null +++ b/thanasoft-back/app/Repositories/ClientRepositoryInterface.php @@ -0,0 +1,12 @@ +model->newQuery()->with('client'); + + // Apply filters + if (!empty($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('first_name', 'like', '%' . $filters['search'] . '%') + ->orWhere('last_name', 'like', '%' . $filters['search'] . '%') + ->orWhere('email', 'like', '%' . $filters['search'] . '%') + ->orWhere('phone', 'like', '%' . $filters['search'] . '%') + ->orWhere('mobile', 'like', '%' . $filters['search'] . '%') + ->orWhere('position', 'like', '%' . $filters['search'] . '%'); + }); + } + + if (isset($filters['is_primary'])) { + $query->where('is_primary', $filters['is_primary']); + } + + if (!empty($filters['client_id'])) { + $query->where('client_id', $filters['client_id']); + } + + // Apply sorting + $sortField = $filters['sort_by'] ?? 'created_at'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + + // Special handling for name sorting + if ($sortField === 'name') { + $query->orderBy('last_name', $sortDirection) + ->orderBy('first_name', $sortDirection); + } else { + $query->orderBy($sortField, $sortDirection); + } + + return $query->paginate($perPage); + } + + public function getByClientId(int $clientId) + { + return $this->model->newQuery() + ->where('client_id', $clientId) + ->get(); + } + + public function getByFournisseurId(int $fournisseurId) + { + return $this->model->newQuery() + ->where('fournisseur_id', $fournisseurId) + ->get(); + } +} diff --git a/thanasoft-back/app/Repositories/ContactRepositoryInterface.php b/thanasoft-back/app/Repositories/ContactRepositoryInterface.php new file mode 100644 index 0000000..3eeb2db --- /dev/null +++ b/thanasoft-back/app/Repositories/ContactRepositoryInterface.php @@ -0,0 +1,14 @@ +model->newQuery()->with(['deceased', 'client', 'vehicle', 'departureLocation']); + + if (! empty($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('mission_title', 'like', '%' . $filters['search'] . '%') + ->orWhere('family_email', 'like', '%' . $filters['search'] . '%') + ->orWhere('departure_name', 'like', '%' . $filters['search'] . '%') + ->orWhere('departure_city', 'like', '%' . $filters['search'] . '%'); + }); + } + + if (! empty($filters['status'])) { + $query->where('status', $filters['status']); + } + + if (! empty($filters['convoy_type'])) { + $query->where('convoy_type', $filters['convoy_type']); + } + + if (! empty($filters['vehicle_id'])) { + $query->where('vehicle_id', $filters['vehicle_id']); + } + + if (! empty($filters['deceased_id'])) { + $query->where('deceased_id', $filters['deceased_id']); + } + + $sortField = $filters['sort_by'] ?? 'planned_start_at'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + + return $query->orderBy($sortField, $sortDirection)->paginate($perPage); + } +} diff --git a/thanasoft-back/app/Repositories/ConvoyRepositoryInterface.php b/thanasoft-back/app/Repositories/ConvoyRepositoryInterface.php new file mode 100644 index 0000000..7bd753c --- /dev/null +++ b/thanasoft-back/app/Repositories/ConvoyRepositoryInterface.php @@ -0,0 +1,12 @@ +model->newQuery()->with(['deceased', 'file']); + + // Apply filters + if (!empty($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('doc_type', 'like', '%' . $filters['search'] . '%') + ->orWhereHas('deceased', function ($q) use ($filters) { + $q->where('first_name', 'like', '%' . $filters['search'] . '%') + ->orWhere('last_name', 'like', '%' . $filters['search'] . '%'); + }); + }); + } + + if (!empty($filters['deceased_id'])) { + $query->where('deceased_id', $filters['deceased_id']); + } + + if (!empty($filters['doc_type'])) { + $query->where('doc_type', $filters['doc_type']); + } + + if (!empty($filters['file_id'])) { + $query->where('file_id', $filters['file_id']); + } + + // Apply sorting + $sortField = $filters['sort_by'] ?? 'created_at'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + $query->orderBy($sortField, $sortDirection); + + return $query->paginate($perPage); + } + + /** + * Get documents by deceased ID + */ + public function getByDeceasedId(int $deceasedId): Collection + { + return $this->model->newQuery() + ->with(['deceased', 'file']) + ->where('deceased_id', $deceasedId) + ->orderBy('generated_at', 'desc') + ->get(); + } + + /** + * Get documents by document type + */ + public function getByDocType(string $docType): Collection + { + return $this->model->newQuery() + ->with(['deceased', 'file']) + ->where('doc_type', $docType) + ->orderBy('generated_at', 'desc') + ->get(); + } + + /** + * Get documents by file ID + */ + public function getByFileId(int $fileId): Collection + { + return $this->model->newQuery() + ->with(['deceased', 'file']) + ->where('file_id', $fileId) + ->orderBy('generated_at', 'desc') + ->get(); + } + + /** + * Search documents by various criteria + */ + public function search(array $criteria): Collection + { + $query = $this->model->newQuery()->with(['deceased', 'file']); + + if (!empty($criteria['deceased_id'])) { + $query->where('deceased_id', $criteria['deceased_id']); + } + + if (!empty($criteria['doc_type'])) { + $query->where('doc_type', $criteria['doc_type']); + } + + if (!empty($criteria['file_id'])) { + $query->where('file_id', $criteria['file_id']); + } + + if (!empty($criteria['generated_from'])) { + $query->where('generated_at', '>=', $criteria['generated_from']); + } + + if (!empty($criteria['generated_to'])) { + $query->where('generated_at', '<=', $criteria['generated_to']); + } + + return $query->orderBy('generated_at', 'desc')->get(); + } +} diff --git a/thanasoft-back/app/Repositories/DeceasedDocumentRepositoryInterface.php b/thanasoft-back/app/Repositories/DeceasedDocumentRepositoryInterface.php new file mode 100644 index 0000000..67ee56f --- /dev/null +++ b/thanasoft-back/app/Repositories/DeceasedDocumentRepositoryInterface.php @@ -0,0 +1,39 @@ +model->newQuery(); + + // Apply filters + if (!empty($filters['search'])) { + $query->where(function($q) use ($filters) { + $q->where('last_name', 'LIKE', "%{$filters['search']}%") + ->orWhere('first_name', 'LIKE', "%{$filters['search']}%"); + }); + } + + // Apply date range filters + if (!empty($filters['start_date'])) { + $query->where('death_date', '>=', $filters['start_date']); + } + + if (!empty($filters['end_date'])) { + $query->where('death_date', '<=', $filters['end_date']); + } + + // Apply sorting + $sortBy = $filters['sort_by'] ?? 'created_at'; + $sortOrder = $filters['sort_order'] ?? 'desc'; + $query->orderBy($sortBy, $sortOrder); + + // Eager load related counts + $query->withCount(['documents', 'interventions']); + + return $query->paginate($perPage); + } + + /** + * Find a deceased by ID + * + * @param int $id + * @return Deceased + */ + public function findById(int $id): Deceased + { + return $this->find($id); + } + + /** + * Search deceased by name + * + * @param string $name + * @return Collection + */ + public function searchByName(string $name): Collection + { + return $this->model->newQuery() + ->where(function($query) use ($name) { + $query->where('last_name', 'LIKE', "%{$name}%") + ->orWhere('first_name', 'LIKE', "%{$name}%"); + }) + ->orderBy('last_name', 'asc') + ->orderBy('first_name', 'asc') + ->get(); + } +} diff --git a/thanasoft-back/app/Repositories/DeceasedRepositoryInterface.php b/thanasoft-back/app/Repositories/DeceasedRepositoryInterface.php new file mode 100644 index 0000000..3fa9bea --- /dev/null +++ b/thanasoft-back/app/Repositories/DeceasedRepositoryInterface.php @@ -0,0 +1,35 @@ +model->newQuery(); + + // Apply filters + if (!empty($filters['search'])) { + $query->search($filters['search']); + } + + if (isset($filters['active'])) { + if ($filters['active']) { + $query->active(); + } else { + $query->inactive(); + } + } + + // Apply sorting + $sortField = $filters['sort_by'] ?? 'last_name'; + $sortDirection = $filters['sort_direction'] ?? 'asc'; + $query->orderBy($sortField, $sortDirection); + + return $query->get(); + } + + /** + * Find an employee by ID. + */ + public function findById(int $id): ?Employee + { + return $this->model->newQuery()->with(['thanatopractitioner', 'user'])->find($id); + } + + /** + * Find an employee by email. + */ + public function findByEmail(string $email): ?Employee + { + return $this->model->newQuery()->where('email', $email)->first(); + } + + /** + * Get active employees only. + */ + public function getActive(): Collection + { + return $this->model->newQuery()->active()->get(); + } + + /** + * Get inactive employees only. + */ + public function getInactive(): Collection + { + return $this->model->newQuery()->inactive()->get(); + } + + /** + * Search employees by term. + */ + public function search(string $term): Collection + { + return $this->model->newQuery()->search($term)->get(); + } + + /** + * Get employees with pagination. + */ + public function getPaginated(int $perPage = 10, array $filters = []): array + { + $query = $this->model->newQuery()->with(['thanatopractitioner', 'user']); + + if (!empty($filters['search'])) { + $query->search($filters['search']); + } + + if (array_key_exists('active', $filters)) { + if (filter_var($filters['active'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) { + $query->active(); + } else { + $query->inactive(); + } + } + + $sortField = $filters['sort_by'] ?? 'last_name'; + $sortDirection = $filters['sort_direction'] ?? 'asc'; + + $paginator = $query + ->orderBy($sortField, $sortDirection) + ->paginate($perPage); + + return [ + 'employees' => $paginator->getCollection(), + 'pagination' => [ + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + 'per_page' => $paginator->perPage(), + 'total' => $paginator->total(), + 'from' => $paginator->firstItem(), + 'to' => $paginator->lastItem(), + ], + ]; + } + + /** + * Get employees with their thanatopractitioner data. + */ + public function getWithThanatopractitioner(): Collection + { + return $this->model->newQuery() + ->with(['thanatopractitioner', 'user']) + ->orderBy('last_name') + ->get(); + } + + public function find(int|string $id, array $columns = ['*']): ?\Illuminate\Database\Eloquent\Model + { + return $this->model->newQuery() + ->with(['thanatopractitioner', 'user']) + ->find($id, $columns); + } + + /** + * Get employee statistics. + */ + public function getStatistics(): array + { + return [ + 'total' => $this->model->newQuery()->count(), + 'active' => $this->model->newQuery()->active()->count(), + 'inactive' => $this->model->newQuery()->inactive()->count(), + 'with_thanatopractitioner' => $this->model->newQuery()->has('thanatopractitioner')->count(), + ]; + } +} diff --git a/thanasoft-back/app/Repositories/EmployeeRepositoryInterface.php b/thanasoft-back/app/Repositories/EmployeeRepositoryInterface.php new file mode 100644 index 0000000..6ff5bab --- /dev/null +++ b/thanasoft-back/app/Repositories/EmployeeRepositoryInterface.php @@ -0,0 +1,83 @@ + $filters + * @return Collection + */ + public function getAll(array $filters = []): Collection; + + /** + * Find an employee by ID. + * + * @param int $id + * @return Employee|null + */ + public function findById(int $id): ?Employee; + + /** + * Find an employee by email. + * + * @param string $email + * @return Employee|null + */ + public function findByEmail(string $email): ?Employee; + + /** + * Get active employees only. + * + * @return Collection + */ + public function getActive(): Collection; + + /** + * Get inactive employees only. + * + * @return Collection + */ + public function getInactive(): Collection; + + /** + * Search employees by term. + * + * @param string $term + * @return Collection + */ + public function search(string $term): Collection; + + /** + * Get employees with pagination. + * + * @param int $perPage + * @param array $filters + * @return array{employees: Collection, pagination: array} + */ + public function getPaginated(int $perPage = 10, array $filters = []): array; + + /** + * Get employees with their thanatopractitioner data. + * + * @return Collection + */ + public function getWithThanatopractitioner(): Collection; + + /** + * Get employee statistics. + * + * @return array + */ + public function getStatistics(): array; +} diff --git a/thanasoft-back/app/Repositories/FileRepository.php b/thanasoft-back/app/Repositories/FileRepository.php new file mode 100644 index 0000000..5acf770 --- /dev/null +++ b/thanasoft-back/app/Repositories/FileRepository.php @@ -0,0 +1,232 @@ +model->newQuery(); + + // Apply filters + if (!empty($filters['search'])) { + $query->where('file_name', 'like', '%' . $filters['search'] . '%'); + } + + if (!empty($filters['mime_type'])) { + $query->where('mime_type', 'like', '%' . $filters['mime_type'] . '%'); + } + + if (!empty($filters['uploaded_by'])) { + $query->where('uploaded_by', $filters['uploaded_by']); + } + + if (!empty($filters['category'])) { + // Extract category from storage_uri path + $query->where('storage_uri', 'like', '%/' . $filters['category'] . '/%'); + } + + if (!empty($filters['client_id'])) { + // Extract client files from storage path + $query->where('storage_uri', 'like', '%/client/' . $filters['client_id'] . '/%'); + } + + // Date range filter + if (!empty($filters['date_from'])) { + $query->whereDate('uploaded_at', '>=', $filters['date_from']); + } + + if (!empty($filters['date_to'])) { + $query->whereDate('uploaded_at', '<=', $filters['date_to']); + } + + // Apply sorting + $sortField = $filters['sort_by'] ?? 'uploaded_at'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + $query->orderBy($sortField, $sortDirection); + + return $query->paginate($perPage); + } + + /** + * Get files by category/type (e.g., devis, facture) + */ + public function getByCategory(string $category, int $perPage = 15): LengthAwarePaginator + { + return $this->model->newQuery() + ->where('storage_uri', 'like', '%/' . $category . '/%') + ->orderBy('uploaded_at', 'desc') + ->paginate($perPage); + } + + /** + * Get files by client ID + */ + public function getByClient(int $clientId, int $perPage = 15): LengthAwarePaginator + { + return $this->model->newQuery() + ->where('storage_uri', 'like', '%/client/' . $clientId . '/%') + ->orderBy('uploaded_at', 'desc') + ->paginate($perPage); + } + + /** + * Get files by user/uploader + */ + public function getByUploader(int $uploaderId, int $perPage = 15): LengthAwarePaginator + { + return $this->model->newQuery() + ->where('uploaded_by', $uploaderId) + ->orderBy('uploaded_at', 'desc') + ->paginate($perPage); + } + + /** + * Search files by filename + */ + public function search(string $searchTerm, int $perPage = 15): LengthAwarePaginator + { + return $this->model->newQuery() + ->where('file_name', 'like', '%' . $searchTerm . '%') + ->orWhere('storage_uri', 'like', '%' . $searchTerm . '%') + ->orderBy('uploaded_at', 'desc') + ->paginate($perPage); + } + + /** + * Get recent files + */ + public function getRecent(int $limit = 10): Collection + { + return $this->model->newQuery() + ->orderBy('uploaded_at', 'desc') + ->limit($limit) + ->get(); + } + + /** + * Get files by storage path pattern + */ + public function getByPathPattern(string $pattern, int $perPage = 15): LengthAwarePaginator + { + return $this->model->newQuery() + ->where('storage_uri', 'like', '%' . $pattern . '%') + ->orderBy('uploaded_at', 'desc') + ->paginate($perPage); + } + + /** + * Get files organized by path structure + */ + public function getOrganizedFiles(): Collection + { + $files = $this->model->newQuery() + ->orderBy('storage_uri') + ->get(); + + // Group files by their organized path structure + $organized = collect(); + + foreach ($files as $file) { + $pathParts = explode('/', $file->storage_uri); + + // Skip if not enough path parts + if (count($pathParts) < 3) { + continue; + } + + // Extract structure like: category/subcategory/filename + $category = $pathParts[count($pathParts) - 3] ?? 'root'; + $subcategory = $pathParts[count($pathParts) - 2] ?? 'general'; + + $key = $category . '/' . $subcategory; + + if (!$organized->has($key)) { + $organized->put($key, collect([ + 'category' => $category, + 'subcategory' => $subcategory, + 'files' => collect(), + 'count' => 0 + ])); + } + + $group = $organized->get($key); + $group['files']->push($file); + $group['count']++; + } + + return $organized; + } + + /** + * Get storage usage statistics + */ + public function getStorageStats(): array + { + $totalFiles = $this->model->newQuery()->count(); + $totalSize = $this->model->newQuery()->sum('size_bytes'); + + $byType = $this->model->newQuery() + ->selectRaw('mime_type, COUNT(*) as count, SUM(size_bytes) as total_size') + ->groupBy('mime_type') + ->get(); + + $byCategory = $this->model->newQuery() + ->selectRaw('storage_uri, COUNT(*) as count, SUM(size_bytes) as total_size') + ->get() + ->map(function ($item) { + $pathParts = explode('/', $item->storage_uri); + $category = $pathParts[count($pathParts) - 3] ?? 'root'; + return [ + 'category' => $category, + 'count' => $item->count, + 'total_size' => $item->total_size, + ]; + }) + ->groupBy('category') + ->map(function ($items) { + return [ + 'count' => $items->sum('count'), + 'total_size' => $items->sum('total_size'), + ]; + }); + + return [ + 'total_files' => $totalFiles, + 'total_size_bytes' => $totalSize, + 'total_size_formatted' => $this->formatBytes($totalSize), + 'by_type' => $byType, + 'by_category' => $byCategory, + ]; + } + + /** + * Format bytes to human readable format + */ + private function formatBytes(int $bytes, int $precision = 2): string + { + if ($bytes === 0) { + return '0 B'; + } + + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + $base = 1024; + $factor = floor((strlen($bytes) - 1) / 3); + + return sprintf("%.{$precision}f", $bytes / pow($base, $factor)) . ' ' . $units[$factor]; + } +} diff --git a/thanasoft-back/app/Repositories/FileRepositoryInterface.php b/thanasoft-back/app/Repositories/FileRepositoryInterface.php new file mode 100644 index 0000000..f2de426 --- /dev/null +++ b/thanasoft-back/app/Repositories/FileRepositoryInterface.php @@ -0,0 +1,46 @@ +model->newQuery(); + + // Apply filters + if (!empty($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('name', 'like', '%' . $filters['search'] . '%') + ->orWhere('email', 'like', '%' . $filters['search'] . '%') + ->orWhere('vat_number', 'like', '%' . $filters['search'] . '%') + ->orWhere('siret', 'like', '%' . $filters['search'] . '%'); + }); + } + + if (isset($filters['is_active'])) { + $query->where('is_active', $filters['is_active']); + } + + // Apply sorting + $sortField = $filters['sort_by'] ?? 'created_at'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + $query->orderBy($sortField, $sortDirection); + + return $query->paginate($perPage); + } + + public function searchByName(string $name, int $perPage = 15, bool $exactMatch = false) + { + $query = $this->model->newQuery(); + + if ($exactMatch) { + $query->where('name', $name); + } else { + $query->where('name', 'like', '%' . $name . '%'); + } + + return $query->get(); + } +} diff --git a/thanasoft-back/app/Repositories/FournisseurRepositoryInterface.php b/thanasoft-back/app/Repositories/FournisseurRepositoryInterface.php new file mode 100644 index 0000000..39875e1 --- /dev/null +++ b/thanasoft-back/app/Repositories/FournisseurRepositoryInterface.php @@ -0,0 +1,12 @@ +model->with(['purchaseOrder', 'warehouse', 'lines.product', 'lines.packaging', 'lines.tvaRate'])->get($columns); + } + + public function find(int|string $id, array $columns = ['*']): ?GoodsReceipt + { + return $this->model->with(['purchaseOrder', 'warehouse', 'lines.product', 'lines.packaging', 'lines.tvaRate'])->find($id, $columns); + } + + public function create(array $attributes): GoodsReceipt + { + return DB::transaction(function () use ($attributes) { + try { + $lines = $attributes['lines'] ?? []; + unset($attributes['lines']); + + $goodsReceipt = parent::create($attributes); + + if (!empty($lines)) { + foreach ($lines as $line) { + $goodsReceipt->lines()->create($line); + } + } + + return $goodsReceipt->load('lines.product', 'lines.packaging', 'lines.tvaRate'); + } catch (\Exception $e) { + Log::error('Error creating GoodsReceipt with lines: ' . $e->getMessage(), [ + 'attributes' => $attributes, + 'exception' => $e + ]); + throw $e; + } + }); + } + + public function update(int|string $id, array $attributes): bool + { + return DB::transaction(function () use ($id, $attributes) { + try { + $goodsReceipt = $this->find($id); + if (!$goodsReceipt) { + return false; + } + + $lines = $attributes['lines'] ?? null; + unset($attributes['lines']); + + $updated = parent::update($id, $attributes); + + if ($lines !== null && $updated) { + $goodsReceipt->lines()->delete(); + foreach ($lines as $line) { + $goodsReceipt->lines()->create($line); + } + } + + return $updated; + } catch (\Exception $e) { + Log::error('Error updating GoodsReceipt with lines: ' . $e->getMessage(), [ + 'id' => $id, + 'attributes' => $attributes, + 'exception' => $e + ]); + throw $e; + } + }); + } +} diff --git a/thanasoft-back/app/Repositories/GoodsReceiptRepositoryInterface.php b/thanasoft-back/app/Repositories/GoodsReceiptRepositoryInterface.php new file mode 100644 index 0000000..1f76efa --- /dev/null +++ b/thanasoft-back/app/Repositories/GoodsReceiptRepositoryInterface.php @@ -0,0 +1,9 @@ +isPractitionerAssigned($interventionId, $practitionerId)) { + // If exists, update the role + $this->updatePractitionerRole($interventionId, $practitionerId, $role); + + // Return the updated record + return InterventionPractitioner::where('intervention_id', $interventionId) + ->where('practitioner_id', $practitionerId) + ->first(); + } + + $assignment = InterventionPractitioner::create([ + 'intervention_id' => $interventionId, + 'practitioner_id' => $practitionerId, + 'role' => $role, + 'assigned_at' => $assignedAt ?: now() + ]); + + Log::info('Intervention-practitioner assignment created', [ + 'intervention_id' => $interventionId, + 'practitioner_id' => $practitionerId, + 'role' => $role, + 'assignment_id' => $assignment->id + ]); + + return $assignment; + } catch (\Exception $e) { + Log::error('Error creating intervention-practitioner assignment', [ + 'intervention_id' => $interventionId, + 'practitioner_id' => $practitionerId, + 'role' => $role, + 'error' => $e->getMessage() + ]); + throw $e; + } + } + + /** + * Remove all practitioner assignments for an intervention. + */ + public function removeAllAssignments(int $interventionId): int + { + $deleted = InterventionPractitioner::where('intervention_id', $interventionId)->delete(); + + Log::info('Removed all practitioner assignments for intervention', [ + 'intervention_id' => $interventionId, + 'deleted_count' => $deleted + ]); + + return $deleted; + } + + /** + * Remove specific practitioner assignment. + */ + public function removeAssignment(int $interventionId, int $practitionerId): int + { + $deleted = InterventionPractitioner::where('intervention_id', $interventionId) + ->where('practitioner_id', $practitionerId) + ->delete(); + + if ($deleted > 0) { + Log::info('Removed intervention-practitioner assignment', [ + 'intervention_id' => $interventionId, + 'practitioner_id' => $practitionerId + ]); + } + + return $deleted; + } + + /** + * Check if a practitioner is already assigned to an intervention. + */ + public function isPractitionerAssigned(int $interventionId, int $practitionerId): bool + { + return InterventionPractitioner::where('intervention_id', $interventionId) + ->where('practitioner_id', $practitionerId) + ->exists(); + } + + /** + * Get all practitioner assignments for an intervention. + */ + public function getAssignmentsForIntervention(int $interventionId) + { + return InterventionPractitioner::with('practitioner') + ->where('intervention_id', $interventionId) + ->get(); + } + + /** + * Get principal practitioners for an intervention. + */ + public function getPrincipalPractitioners(int $interventionId) + { + return InterventionPractitioner::with('practitioner') + ->where('intervention_id', $interventionId) + ->principal() + ->get(); + } + + /** + * Get assistant practitioners for an intervention. + */ + public function getAssistantPractitioners(int $interventionId) + { + return InterventionPractitioner::with('practitioner') + ->where('intervention_id', $interventionId) + ->assistant() + ->get(); + } + + /** + * Update practitioner role for an intervention. + */ + public function updatePractitionerRole(int $interventionId, int $practitionerId, string $role): bool + { + $updated = InterventionPractitioner::where('intervention_id', $interventionId) + ->where('practitioner_id', $practitionerId) + ->update([ + 'role' => $role, + 'assigned_at' => now() + ]); + + if ($updated > 0) { + Log::info('Updated practitioner role for intervention', [ + 'intervention_id' => $interventionId, + 'practitioner_id' => $practitionerId, + 'new_role' => $role + ]); + } + + return $updated > 0; + } +} diff --git a/thanasoft-back/app/Repositories/InterventionPractitionerRepositoryInterface.php b/thanasoft-back/app/Repositories/InterventionPractitionerRepositoryInterface.php new file mode 100644 index 0000000..61d52bb --- /dev/null +++ b/thanasoft-back/app/Repositories/InterventionPractitionerRepositoryInterface.php @@ -0,0 +1,79 @@ +where('client_id', $filters['client_id']); + } + + if (!empty($filters['deceased_id'])) { + $query->where('deceased_id', $filters['deceased_id']); + } + + if (!empty($filters['status'])) { + $query->where('status', $filters['status']); + } + + if (!empty($filters['type'])) { + $query->where('type', $filters['type']); + } + + // Date range filters + if (!empty($filters['start_date'])) { + $query->where('scheduled_at', '>=', $filters['start_date']); + } + + if (!empty($filters['end_date'])) { + $query->where('scheduled_at', '<=', $filters['end_date']); + } + + // Apply sorting + $sortBy = $filters['sort_by'] ?? 'created_at'; + $sortOrder = $filters['sort_order'] ?? 'desc'; + $query->orderBy($sortBy, $sortOrder); + + // Eager load related models + $query->with([ + 'client', + 'deceased', + 'location', + 'practitioners' + ]); + + return $query->paginate($perPage); + } + + /** + * Find an intervention by ID + * + * @param int $id + * @return Intervention + */ + public function findById(int $id): Intervention + { + return Intervention::with([ + 'client', + 'deceased', + 'location', + 'practitioners', + 'attachments', + 'notifications', + 'quote', + 'quote.client', + 'quote.lines', + 'quote.history' + ])->findOrFail($id); + } + + /** + * Create a new intervention record + * + * @param array $data + * @return Intervention + */ + public function create(array $data): Intervention + { + return DB::transaction(function () use ($data) { + $intervention = Intervention::create($data); + + try { + $this->timelineRepository->logActivity([ + 'client_id' => $intervention->client_id, + 'actor_type' => 'user', + 'actor_user_id' => auth()->id(), + 'event_type' => 'intervention_created', + 'entity_type' => 'intervention', + 'entity_id' => $intervention->id, + 'title' => 'Nouvelle intervention créée', + 'description' => "Une intervention de type '{$intervention->type}' a été créée.", + 'created_at' => now(), + ]); + } catch (\Exception $e) { + Log::error("Failed to log intervention creation activity: " . $e->getMessage()); + } + + return $intervention; + }); + } + + /** + * Update an existing intervention record + * + * @param Intervention $intervention + * @param array $data + * @return Intervention + */ + public function update(Intervention $intervention, array $data): Intervention + { + return DB::transaction(function () use ($intervention, $data) { + $intervention->update($data); + return $intervention; + }); + } + + /** + * Delete an intervention record + * + * @param Intervention $intervention + * @return bool + */ + public function delete(Intervention $intervention): bool + { + return DB::transaction(function () use ($intervention) { + return $intervention->delete(); + }); + } + + /** + * Change the status of an intervention + * + * @param Intervention $intervention + * @param string $status + * @return Intervention + */ + public function changeStatus(Intervention $intervention, string $status): Intervention + { + return DB::transaction(function () use ($intervention, $status) { + $intervention->update(['status' => $status]); + return $intervention; + }); + } + + /** + * Get interventions for a specific month + * + * @param int $year + * @param int $month + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getByMonth(int $year, int $month): \Illuminate\Database\Eloquent\Collection + { + $startDate = sprintf('%04d-%02d-01', $year, $month); + $endDate = date('Y-m-t', strtotime($startDate)); + + return Intervention::query() + ->whereBetween('scheduled_at', [$startDate . ' 00:00:00', $endDate . ' 23:59:59']) + ->with(['client', 'deceased', 'location', 'practitioners']) + ->orderBy('scheduled_at', 'asc') + ->get(); + } + + /** + * Add practitioners to an intervention + * + * @param Intervention $intervention + * @param array $practitionerData + * @return array Array with 'principal' and 'assistant' results + */ + public function addPractitioners(Intervention $intervention, array $practitionerData): array + { + // This method is deprecated in favor of using InterventionPractitionerRepository directly + // Implementation kept for interface compatibility + $results = [ + 'principal' => null, + 'assistant' => [] + ]; + + if (isset($practitionerData['principal_practitioner_id'])) { + $results['principal'] = [ + 'id' => $practitionerData['principal_practitioner_id'], + 'status' => 'handled_by_practitioner_repository' + ]; + } + + if (isset($practitionerData['assistant_practitioner_ids']) && is_array($practitionerData['assistant_practitioner_ids'])) { + foreach ($practitionerData['assistant_practitioner_ids'] as $assistantId) { + $results['assistant'][] = [ + 'id' => $assistantId, + 'status' => 'handled_by_practitioner_repository' + ]; + } + } + + return $results; + } +} diff --git a/thanasoft-back/app/Repositories/InterventionRepositoryInterface.php b/thanasoft-back/app/Repositories/InterventionRepositoryInterface.php new file mode 100644 index 0000000..ec24142 --- /dev/null +++ b/thanasoft-back/app/Repositories/InterventionRepositoryInterface.php @@ -0,0 +1,78 @@ +find($id); + if ($line) { + return $line->update($data); + } + return false; + } + + public function delete(string $id): bool + { + $line = $this->find($id); + if ($line) { + return $line->delete(); + } + return false; + } + + public function find(string $id): ?InvoiceLine + { + return InvoiceLine::find($id); + } + + public function getByInvoiceId(string $invoiceId) + { + return InvoiceLine::where('invoice_id', $invoiceId)->get(); + } +} diff --git a/thanasoft-back/app/Repositories/InvoiceLineRepositoryInterface.php b/thanasoft-back/app/Repositories/InvoiceLineRepositoryInterface.php new file mode 100644 index 0000000..5473ce6 --- /dev/null +++ b/thanasoft-back/app/Repositories/InvoiceLineRepositoryInterface.php @@ -0,0 +1,14 @@ +find($quoteId); + + if (!$quote) { + throw new \Exception("Quote not found"); + } + + $existingInvoice = Invoice::where('source_quote_id', $quote->id)->first(); + if ($existingInvoice) { + return $existingInvoice; + } + + // Create Invoice + $invoiceData = [ + 'client_id' => $quote->client_id, + 'group_id' => $quote->group_id, + 'source_quote_id' => $quote->id, + 'status' => 'brouillon', // Start as draft + 'invoice_date' => now(), + 'currency' => $quote->currency, + 'total_ht' => $quote->total_ht, + 'total_tva' => $quote->total_tva, + 'total_ttc' => $quote->total_ttc, + ]; + + $invoice = parent::create($invoiceData); + + // Copy Lines + foreach ($quote->lines as $line) { + $this->invoiceLineRepository->create([ + 'invoice_id' => $invoice->id, + 'product_id' => $line->product_id, + 'packaging_id' => $line->packaging_id, + 'packages_qty' => $line->packages_qty, + 'units_qty' => $line->units_qty, + 'description' => $line->description, + 'qty_base' => $line->qty_base, + 'unit_price' => $line->unit_price, + 'unit_price_per_package' => $line->unit_price_per_package, + 'tva_rate_id' => $line->tva_rate_id, + 'discount_pct' => $line->discount_pct, + 'total_ht' => $line->total_ht, + ]); + } + + // Record history + $this->recordHistory($invoice->id, null, 'brouillon', 'Created from Quote ' . $quote->reference); + + try { + if ($invoice->client_id !== null) { + $this->timelineRepository->logActivity([ + 'client_id' => $invoice->client_id, + 'actor_type' => 'user', + 'actor_user_id' => auth()->id(), + 'event_type' => 'invoice_created', + 'entity_type' => 'invoice', + 'entity_id' => $invoice->id, + 'title' => 'Nouvelle facture créée', + 'description' => "Une facture a été créée à partir du devis #{$quote->id}.", + 'created_at' => now(), + ]); + } + } catch (\Exception $e) { + Log::error("Failed to log invoice creation activity: " . $e->getMessage()); + } + + return $invoice; + }); + } + + + + public function all(array $columns = ['*']): \Illuminate\Support\Collection + { + return $this->model->with(['client', 'group', 'lines.product'])->get($columns); + } + + public function create(array $data): Invoice + { + return DB::transaction(function () use ($data) { + try { + // Create the invoice + $invoice = parent::create($data); + + // Create the invoice lines + if (isset($data['lines']) && is_array($data['lines'])) { + foreach ($data['lines'] as $lineData) { + $lineData['invoice_id'] = $invoice->id; + $this->invoiceLineRepository->create($lineData); + } + } + + // Record initial status history + $this->recordHistory($invoice->id, null, $invoice->status, 'Invoice created'); + + try { + if ($invoice->client_id !== null) { + $this->timelineRepository->logActivity([ + 'client_id' => $invoice->client_id, + 'actor_type' => 'user', + 'actor_user_id' => auth()->id(), + 'event_type' => 'invoice_created', + 'entity_type' => 'invoice', + 'entity_id' => $invoice->id, + 'title' => 'Nouvelle facture créée', + 'description' => "La facture #{$invoice->id} a été créée.", + 'created_at' => now(), + ]); + } + } catch (\Exception $e) { + Log::error("Failed to log manual invoice creation activity: " . $e->getMessage()); + } + + return $invoice; + } catch (\Exception $e) { + Log::error('Error creating invoice with lines: ' . $e->getMessage(), [ + 'exception' => $e, + 'data' => $data, + ]); + throw $e; + } + }); + } + + public function update(int|string $id, array $attributes): bool + { + return DB::transaction(function () use ($id, $attributes) { + try { + $invoice = $this->find($id); + if (!$invoice) { + return false; + } + + $oldStatus = $invoice->status; + + // Update the invoice + $updated = parent::update($id, $attributes); + + if ($updated) { + $newStatus = $attributes['status'] ?? $oldStatus; + + // If status changed, record history + if ($oldStatus !== $newStatus) { + $this->recordHistory((int) $id, $oldStatus, $newStatus, 'Invoice status updated'); + } + } + + return $updated; + } catch (\Exception $e) { + Log::error('Error updating invoice: ' . $e->getMessage(), [ + 'id' => $id, + 'attributes' => $attributes, + 'exception' => $e, + ]); + throw $e; + } + }); + } + + public function find(int|string $id, array $columns = ['*']): ?Invoice + { + return $this->model->with(['client', 'group', 'lines.product', 'history.user'])->find($id, $columns); + } + + private function recordHistory(int $invoiceId, ?string $oldStatus, string $newStatus, ?string $comment = null): void + { + \App\Models\DocumentStatusHistory::create([ + 'document_type' => 'invoice', + 'document_id' => $invoiceId, + 'old_status' => $oldStatus, + 'new_status' => $newStatus, + 'changed_by' => auth()->id(), // Assuming authenticated user + 'comment' => $comment, + 'changed_at' => now(), + ]); + } +} diff --git a/thanasoft-back/app/Repositories/InvoiceRepositoryInterface.php b/thanasoft-back/app/Repositories/InvoiceRepositoryInterface.php new file mode 100644 index 0000000..5f61598 --- /dev/null +++ b/thanasoft-back/app/Repositories/InvoiceRepositoryInterface.php @@ -0,0 +1,10 @@ +model->newQuery()->with(['thanatopractitioner.employee']); + + // Apply filters + if (!empty($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('doc_type', 'like', '%' . $filters['search'] . '%') + ->orWhere('status', 'like', '%' . $filters['search'] . '%'); + }); + } + + if (!empty($filters['practitioner_id'])) { + $query->where('practitioner_id', $filters['practitioner_id']); + } + + if (!empty($filters['doc_type'])) { + $query->ofType($filters['doc_type']); + } + + if (isset($filters['valid_only'])) { + if ($filters['valid_only']) { + $query->valid(); + } else { + $query->expired(); + } + } + + // Apply sorting + $sortField = $filters['sort_by'] ?? 'created_at'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + $query->orderBy($sortField, $sortDirection); + + return $query->get(); + } + + /** + * Find a practitioner document by ID. + */ + public function findById(int $id): ?PractitionerDocument + { + return $this->model->newQuery() + ->with(['thanatopractitioner.employee']) + ->find($id); + } + + /** + * Get documents by practitioner ID. + */ + public function getByPractitionerId(int $practitionerId): Collection + { + return $this->model->newQuery() + ->where('practitioner_id', $practitionerId) + ->orderBy('created_at', 'desc') + ->get(); + } + + /** + * Get documents by type. + */ + public function getByDocumentType(string $docType): Collection + { + return $this->model->newQuery() + ->with(['thanatopractitioner.employee']) + ->ofType($docType) + ->orderBy('created_at', 'desc') + ->get(); + } + + /** + * Get valid documents (not expired). + */ + public function getValid(): Collection + { + return $this->model->newQuery() + ->with(['thanatopractitioner.employee']) + ->valid() + ->orderBy('expiry_date') + ->get(); + } + + /** + * Get expired documents. + */ + public function getExpired(): Collection + { + return $this->model->newQuery() + ->with(['thanatopractitioner.employee']) + ->expired() + ->orderBy('expiry_date', 'desc') + ->get(); + } + + /** + * Get documents with pagination. + */ + public function getPaginated(int $perPage = 10): array + { + $paginator = $this->model->newQuery() + ->with(['thanatopractitioner.employee']) + ->paginate($perPage); + + return [ + 'documents' => $paginator->getCollection(), + 'pagination' => [ + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + 'per_page' => $paginator->perPage(), + 'total' => $paginator->total(), + ], + ]; + } + + /** + * Get document statistics. + */ + public function getStatistics(): array + { + return [ + 'total' => $this->model->newQuery()->count(), + 'valid' => $this->model->newQuery()->valid()->count(), + 'expired' => $this->model->newQuery()->expired()->count(), + 'by_type' => $this->model->newQuery() + ->selectRaw('doc_type, count(*) as count') + ->groupBy('doc_type') + ->pluck('count', 'doc_type') + ->toArray(), + ]; + } +} diff --git a/thanasoft-back/app/Repositories/PractitionerDocumentRepositoryInterface.php b/thanasoft-back/app/Repositories/PractitionerDocumentRepositoryInterface.php new file mode 100644 index 0000000..77b3116 --- /dev/null +++ b/thanasoft-back/app/Repositories/PractitionerDocumentRepositoryInterface.php @@ -0,0 +1,75 @@ + $filters + * @return Collection + */ + public function getAll(array $filters = []): Collection; + + /** + * Find a practitioner document by ID. + * + * @param int $id + * @return PractitionerDocument|null + */ + public function findById(int $id): ?PractitionerDocument; + + /** + * Get documents by practitioner ID. + * + * @param int $practitionerId + * @return Collection + */ + public function getByPractitionerId(int $practitionerId): Collection; + + /** + * Get documents by type. + * + * @param string $docType + * @return Collection + */ + public function getByDocumentType(string $docType): Collection; + + /** + * Get valid documents (not expired). + * + * @return Collection + */ + public function getValid(): Collection; + + /** + * Get expired documents. + * + * @return Collection + */ + public function getExpired(): Collection; + + /** + * Get documents with pagination. + * + * @param int $perPage + * @return array{documents: Collection, pagination: array} + */ + public function getPaginated(int $perPage = 10): array; + + /** + * Get document statistics. + * + * @return array + */ + public function getStatistics(): array; +} diff --git a/thanasoft-back/app/Repositories/PriceListRepository.php b/thanasoft-back/app/Repositories/PriceListRepository.php new file mode 100644 index 0000000..e827e3d --- /dev/null +++ b/thanasoft-back/app/Repositories/PriceListRepository.php @@ -0,0 +1,15 @@ +model->active()->orderBy('name')->get(); + } + + /** + * Get root categories (no parent). + */ + public function getRoots(): Collection + { + return $this->model->roots()->orderBy('name')->get(); + } + + /** + * Get categories with their children. + */ + public function getWithChildren(): Collection + { + return $this->model->with('children') + ->roots() + ->orderBy('name') + ->get(); + } + + /** + * Get all product categories with pagination + */ + public function paginate(int $perPage = 15, array $filters = []): LengthAwarePaginator + { + $query = $this->model->newQuery(); + + if (!empty($filters['search'])) { + $search = $filters['search']; + $query->where(function (Builder $q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('code', 'like', "%{$search}%") + ->orWhere('description', 'like', "%{$search}%"); + }); + } + + if (isset($filters['active']) && $filters['active'] !== '') { + $query->where('active', (bool) $filters['active']); + } + + if (isset($filters['parent_id']) && $filters['parent_id'] !== '') { + if ($filters['parent_id'] === 'null') { + $query->whereNull('parent_id'); + } else { + $query->where('parent_id', $filters['parent_id']); + } + } + + $sortBy = $filters['sort_by'] ?? 'name'; + $sortDirection = $filters['sort_direction'] ?? 'asc'; + $query->orderBy($sortBy, $sortDirection); + + return $query->paginate($perPage); + } + + /** + * Search categories by name or code. + * BaseRepository usually has a general search, but this one is specific with 'code' and 'description'. + */ + public function search(string $term, int $perPage = 15): LengthAwarePaginator + { + return $this->model->where(function ($query) use ($term) { + $query->where('name', 'like', "%{$term}%") + ->orWhere('code', 'like', "%{$term}%") + ->orWhere('description', 'like', "%{$term}%"); + }) + ->orderBy('name') + ->paginate($perPage); + } + + /** + * Get category statistics. + */ + public function getStatistics(): array + { + $total = $this->model->count(); + $active = $this->model->active()->count(); + $inactive = $total - $active; + $withChildren = $this->model->has('children')->count(); + $rootCategories = $this->model->roots()->count(); + + return [ + 'total_categories' => $total, + 'active_categories' => $active, + 'inactive_categories' => $inactive, + 'categories_with_children' => $withChildren, + 'root_categories' => $rootCategories, + ]; + } + + /** + * Check if category can be deleted. + */ + public function canDelete($id): bool + { + $category = $this->find($id); + + if (!$category) { + return false; + } + + // Cannot delete if has children + if ($category->hasChildren()) { + return false; + } + + // Cannot delete if has products + if ($category->hasProducts()) { + return false; + } + + return true; + } +} diff --git a/thanasoft-back/app/Repositories/ProductCategoryRepositoryInterface.php b/thanasoft-back/app/Repositories/ProductCategoryRepositoryInterface.php new file mode 100644 index 0000000..3ed0034 --- /dev/null +++ b/thanasoft-back/app/Repositories/ProductCategoryRepositoryInterface.php @@ -0,0 +1,45 @@ +model->newQuery()->with(['fournisseur', 'category']); + + // Apply filters + if (!empty($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('nom', 'like', '%' . $filters['search'] . '%') + ->orWhere('reference', 'like', '%' . $filters['search'] . '%') + ->orWhere('fabricant', 'like', '%' . $filters['search'] . '%'); + }); + } + + if (!empty($filters['categorie'])) { + $query->where('categorie_id', $filters['categorie']); + } + + if (!empty($filters['fournisseur_id'])) { + $query->where('fournisseur_id', $filters['fournisseur_id']); + } + + if (isset($filters['low_stock'])) { + $query->whereRaw('stock_actuel <= stock_minimum'); + } + + if (isset($filters['expiring_soon'])) { + $query->where('date_expiration', '<=', now()->addDays(30)->toDateString()) + ->where('date_expiration', '>=', now()->toDateString()); + } + + if (isset($filters['is_intervention'])) { + $query->whereHas('category', function ($q) use ($filters) { + $q->where('intervention', filter_var($filters['is_intervention'], FILTER_VALIDATE_BOOLEAN)); + }); + } + + // Apply sorting + $sortField = $filters['sort_by'] ?? 'created_at'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + $query->orderBy($sortField, $sortDirection); + + return $query->paginate($perPage); + } + + /** + * Get products with low stock + */ + public function getLowStockProducts(int $perPage = 15): LengthAwarePaginator + { + return $this->model->newQuery() + ->with(['fournisseur', 'category']) + ->whereRaw('stock_actuel <= stock_minimum') + ->orderBy('stock_actuel', 'asc') + ->paginate($perPage); + } + + /** + * Search products by name + */ + public function searchByName(string $name, int $perPage = 15, bool $exactMatch = false) + { + $query = $this->model->newQuery()->with(['fournisseur', 'category']); + + if ($exactMatch) { + $query->where('nom', $name); + } + else { + $query->where('nom', 'like', '%' . $name . '%'); + } + + return $query->get(); + } + + /** + * Get products by category + */ + public function getByCategory(int $categoryId, int $perPage = 15): LengthAwarePaginator + { + return $this->model->newQuery() + ->with(['fournisseur', 'category']) + ->where('categorie_id', $categoryId) + ->orderBy('nom') + ->paginate($perPage); + } + + /** + * Get products by fournisseur + */ + public function getProductsByFournisseur(int $fournisseurId): LengthAwarePaginator + { + return $this->model->newQuery() + ->where('fournisseur_id', $fournisseurId) + ->orderBy('nom') + ->paginate(15); + } + + /** + * Update stock quantity + */ + public function updateStock(int $productId, float $newQuantity): bool + { + return $this->model->where('id', $productId) + ->update(['stock_actuel' => $newQuantity]) > 0; + } + + /** + * Get product statistics + */ + public function getStatistics(): array + { + $totalProducts = $this->model->count(); + $lowStockProducts = $this->model->whereRaw('stock_actuel <= stock_minimum')->count(); + $expiringProducts = $this->model->where('date_expiration', '<=', now()->addDays(30)->toDateString()) + ->where('date_expiration', '>=', now()->toDateString()) + ->count(); + $totalValue = $this->model->sum(\DB::raw('stock_actuel * prix_unitaire')); + + return [ + 'total_products' => $totalProducts, + 'low_stock_products' => $lowStockProducts, + 'expiring_products' => $expiringProducts, + 'total_value' => $totalValue, + ]; + } + /** + * Find a default intervention product (where category has intervention=true) + */ + public function findInterventionProduct(): ?Product + { + return $this->model->newQuery() + ->whereHas('category', function ($query) { + $query->where('intervention', true); + }) + ->first(); + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Repositories/ProductRepositoryInterface.php b/thanasoft-back/app/Repositories/ProductRepositoryInterface.php new file mode 100644 index 0000000..961e047 --- /dev/null +++ b/thanasoft-back/app/Repositories/ProductRepositoryInterface.php @@ -0,0 +1,22 @@ +model->with(['fournisseur', 'lines.product'])->get($columns); + } + + public function find(int|string $id, array $columns = ['*']): ?PurchaseOrder + { + return $this->model->with(['fournisseur', 'lines.product'])->find($id, $columns); + } + + public function create(array $attributes): PurchaseOrder + { + return DB::transaction(function () use ($attributes) { + try { + $lines = $attributes['lines'] ?? []; + unset($attributes['lines']); + + $purchaseOrder = parent::create($attributes); + + foreach ($lines as $line) { + $purchaseOrder->lines()->create($line); + } + + return $purchaseOrder->load('lines.product'); + } catch (\Exception $e) { + Log::error('Error creating PurchaseOrder with lines: ' . $e->getMessage(), [ + 'attributes' => $attributes, + 'exception' => $e + ]); + throw $e; + } + }); + } + + public function update(int|string $id, array $attributes): bool + { + return DB::transaction(function () use ($id, $attributes) { + try { + $purchaseOrder = $this->find($id); + if (!$purchaseOrder) { + return false; + } + + $lines = $attributes['lines'] ?? null; + unset($attributes['lines']); + + $updated = parent::update($id, $attributes); + + if ($lines !== null && $updated) { + $purchaseOrder->lines()->delete(); + foreach ($lines as $line) { + $purchaseOrder->lines()->create($line); + } + } + + return $updated; + } catch (\Exception $e) { + Log::error('Error updating PurchaseOrder with lines: ' . $e->getMessage(), [ + 'id' => $id, + 'attributes' => $attributes, + 'exception' => $e + ]); + throw $e; + } + }); + } +} diff --git a/thanasoft-back/app/Repositories/PurchaseOrderRepositoryInterface.php b/thanasoft-back/app/Repositories/PurchaseOrderRepositoryInterface.php new file mode 100644 index 0000000..78c8c01 --- /dev/null +++ b/thanasoft-back/app/Repositories/PurchaseOrderRepositoryInterface.php @@ -0,0 +1,9 @@ +find($id); + if ($line) { + return $line->update($data); + } + return false; + } + + public function delete(string $id): bool + { + $line = $this->find($id); + if ($line) { + return $line->delete(); + } + return false; + } + + public function find(string $id): ?QuoteLine + { + return QuoteLine::find($id); + } + + public function getByQuoteId(string $quoteId) + { + return QuoteLine::where('quote_id', $quoteId)->get(); + } +} diff --git a/thanasoft-back/app/Repositories/QuoteLineRepositoryInterface.php b/thanasoft-back/app/Repositories/QuoteLineRepositoryInterface.php new file mode 100644 index 0000000..88e4b04 --- /dev/null +++ b/thanasoft-back/app/Repositories/QuoteLineRepositoryInterface.php @@ -0,0 +1,14 @@ +model->with(['client', 'group', 'lines.product'])->get($columns); + } + + public function create(array $data): Quote + { + return DB::transaction(function () use ($data) { + try { + // Create the quote + $quote = parent::create($data); + + // Create the quote lines + if (isset($data['lines']) && is_array($data['lines'])) { + foreach ($data['lines'] as $lineData) { + $lineData['quote_id'] = $quote->id; + $this->quoteLineRepository->create($lineData); + } + } + + // Record initial status history + $this->recordHistory($quote->id, null, $quote->status, 'Quote created'); + + try { + $this->timelineRepository->logActivity([ + 'client_id' => $quote->client_id, + 'actor_type' => 'user', + 'actor_user_id' => auth()->id(), + 'event_type' => 'quote_created', + 'entity_type' => 'quote', + 'entity_id' => $quote->id, + 'title' => 'Nouveau devis créé', + 'description' => "Le devis #{$quote->id} a été créé.", + 'created_at' => now(), + ]); + } catch (\Exception $e) { + Log::error("Failed to log quote creation activity: " . $e->getMessage()); + } + + return $quote; + } catch (\Exception $e) { + Log::error('Error creating quote with lines: ' . $e->getMessage(), [ + 'exception' => $e, + 'data' => $data, + ]); + throw $e; + } + }); + } + + public function update(int|string $id, array $attributes): bool + { + return DB::transaction(function () use ($id, $attributes) { + try { + $quote = $this->find($id); + if (!$quote) { + return false; + } + + $oldStatus = $quote->status; + + // Update the quote + $updated = parent::update($id, $attributes); + + if ($updated) { + $newStatus = $attributes['status'] ?? $oldStatus; + + // If status changed, record history + if ($oldStatus !== $newStatus) { + $this->recordHistory((int) $id, $oldStatus, $newStatus, 'Quote status updated'); + + // Auto-create invoice when status changes to 'accepte' + if ($newStatus === 'accepte' && $oldStatus !== 'accepte') { + $this->invoiceRepository->createFromQuote($id); + Log::info('Invoice auto-created from quote', ['quote_id' => $id]); + } + } + } + + return $updated; + } catch (\Exception $e) { + Log::error('Error updating quote: ' . $e->getMessage(), [ + 'id' => $id, + 'attributes' => $attributes, + 'exception' => $e, + ]); + throw $e; + } + }); + } + + public function find(int|string $id, array $columns = ['*']): ?Quote + { + return $this->model->with(['client', 'group', 'lines.product', 'history.user'])->find($id, $columns); + } + + private function recordHistory(int $quoteId, ?string $oldStatus, string $newStatus, ?string $comment = null): void + { + \App\Models\DocumentStatusHistory::create([ + 'document_type' => 'quote', + 'document_id' => $quoteId, + 'old_status' => $oldStatus, + 'new_status' => $newStatus, + 'changed_by' => auth()->id(), // Assuming authenticated user + 'comment' => $comment, + 'changed_at' => now(), + ]); + } +} diff --git a/thanasoft-back/app/Repositories/QuoteRepositoryInterface.php b/thanasoft-back/app/Repositories/QuoteRepositoryInterface.php new file mode 100644 index 0000000..f64bde9 --- /dev/null +++ b/thanasoft-back/app/Repositories/QuoteRepositoryInterface.php @@ -0,0 +1,9 @@ +model->newQuery()->with(['employee']); + + // Apply filters + if (!empty($filters['search'])) { + $query->whereHas('employee', function ($q) use ($filters) { + $q->search($filters['search']); + }); + } + + if (isset($filters['valid_authorization'])) { + if ($filters['valid_authorization']) { + $query->withValidAuthorization(); + } else { + $query->withExpiredAuthorization(); + } + } + + // Apply sorting + $sortField = $filters['sort_by'] ?? 'created_at'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + $query->orderBy($sortField, $sortDirection); + + return $query->get(); + } + + /** + * Find a thanatopractitioner by ID. + */ + public function findById(int $id): ?Thanatopractitioner + { + return $this->model->newQuery() + ->with(['employee', 'documents']) + ->find($id); + } + + /** + * Find a thanatopractitioner by employee ID. + */ + public function findByEmployeeId(int $employeeId): ?Thanatopractitioner + { + return $this->model->newQuery() + ->with(['employee', 'documents']) + ->where('employee_id', $employeeId) + ->first(); + } + + /** + * Get thanatopractitioners with valid authorization. + */ + public function getWithValidAuthorization(): Collection + { + return $this->model->newQuery() + ->with(['employee']) + ->withValidAuthorization() + ->orderBy('authorization_expiry_date') + ->get(); + } + + /** + * Get thanatopractitioners with expired authorization. + */ + public function getWithExpiredAuthorization(): Collection + { + return $this->model->newQuery() + ->with(['employee']) + ->withExpiredAuthorization() + ->orderBy('authorization_expiry_date', 'desc') + ->get(); + } + + /** + * Get thanatopractitioners with their complete data. + */ + public function getWithRelations(): Collection + { + return $this->model->newQuery() + ->with(['employee', 'documents']) + ->orderBy('created_at', 'desc') + ->get(); + } + + /** + * Get thanatopractitioners with pagination. + */ + public function getPaginated(int $perPage = 10): array + { + $paginator = $this->model->newQuery() + ->with(['employee']) + ->paginate($perPage); + + return [ + 'thanatopractitioners' => $paginator->getCollection(), + 'pagination' => [ + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + 'per_page' => $paginator->perPage(), + 'total' => $paginator->total(), + ], + ]; + } + + /** + * Get thanatopractitioner statistics. + */ + public function getStatistics(): array + { + return [ + 'total' => $this->model->newQuery()->count(), + 'with_valid_authorization' => $this->model->newQuery()->withValidAuthorization()->count(), + 'with_expired_authorization' => $this->model->newQuery()->withExpiredAuthorization()->count(), + 'with_documents' => $this->model->newQuery()->has('documents')->count(), + ]; + } + + /** + * Search thanatopractitioners by employee name. + */ + public function searchByEmployeeName(string $query): Collection + { + return $this->model->newQuery() + ->with(['employee']) + ->whereHas('employee', function ($q) use ($query) { + $q->where('first_name', 'LIKE', "%{$query}%") + ->orWhere('last_name', 'LIKE', "%{$query}%") + ->orWhereRaw("CONCAT(first_name, ' ', last_name) LIKE ?", ["%{$query}%"]); + }) + ->limit(10) + ->get(); + } +} diff --git a/thanasoft-back/app/Repositories/ThanatopractitionerRepositoryInterface.php b/thanasoft-back/app/Repositories/ThanatopractitionerRepositoryInterface.php new file mode 100644 index 0000000..01849f9 --- /dev/null +++ b/thanasoft-back/app/Repositories/ThanatopractitionerRepositoryInterface.php @@ -0,0 +1,83 @@ + $filters + * @return Collection + */ + public function getAll(array $filters = []): Collection; + + /** + * Find a thanatopractitioner by ID. + * + * @param int $id + * @return Thanatopractitioner|null + */ + public function findById(int $id): ?Thanatopractitioner; + + /** + * Find a thanatopractitioner by employee ID. + * + * @param int $employeeId + * @return Thanatopractitioner|null + */ + public function findByEmployeeId(int $employeeId): ?Thanatopractitioner; + + /** + * Get thanatopractitioners with valid authorization. + * + * @return Collection + */ + public function getWithValidAuthorization(): Collection; + + /** + * Get thanatopractitioners with expired authorization. + * + * @return Collection + */ + public function getWithExpiredAuthorization(): Collection; + + /** + * Get thanatopractitioners with their complete data. + * + * @return Collection + */ + public function getWithRelations(): Collection; + + /** + * Get thanatopractitioners with pagination. + * + * @param int $perPage + * @return array{thanatopractitioners: Collection, pagination: array} + */ + public function getPaginated(int $perPage = 10): array; + + /** + * Get thanatopractitioner statistics. + * + * @return array + */ + + /** + * Search thanatopractitioners by employee name. + * + * @param string $query + * @return Collection + */ + public function searchByEmployeeName(string $query): Collection; + + public function getStatistics(): array; +} diff --git a/thanasoft-back/app/Repositories/TvaRateRepository.php b/thanasoft-back/app/Repositories/TvaRateRepository.php new file mode 100644 index 0000000..13b0a36 --- /dev/null +++ b/thanasoft-back/app/Repositories/TvaRateRepository.php @@ -0,0 +1,26 @@ +model->get($columns); + } + + public function find(int|string $id, array $columns = ['*']): ?TvaRate + { + return $this->model->find($id, $columns); + } +} diff --git a/thanasoft-back/app/Repositories/TvaRateRepositoryInterface.php b/thanasoft-back/app/Repositories/TvaRateRepositoryInterface.php new file mode 100644 index 0000000..f633087 --- /dev/null +++ b/thanasoft-back/app/Repositories/TvaRateRepositoryInterface.php @@ -0,0 +1,9 @@ + $attributes + */ + public function create(array $attributes): Model + { + try { + DB::beginTransaction(); + + $roles = $attributes['roles'] ?? []; + $permissions = $attributes['permissions'] ?? []; + + unset($attributes['roles'], $attributes['permissions']); + + /** @var User $user */ + $user = $this->model->newQuery()->create($attributes); + + if (! empty($roles)) { + $user->syncRoles($roles); + } + + if (! empty($permissions)) { + $user->syncPermissions($permissions); + } + + DB::commit(); + + return $user; + } catch (\Exception $e) { + DB::rollBack(); + Log::error('Error creating user with roles/permissions: ' . $e->getMessage(), [ + 'attributes' => $attributes, + 'exception' => $e, + ]); + throw $e; + } + } + + /** + * @param int|string $id + * @param array $attributes + */ + public function update(int|string $id, array $attributes): bool + { + try { + DB::beginTransaction(); + + /** @var User|null $user */ + $user = $this->find($id); + + if (! $user instanceof User) { + DB::rollBack(); + return false; + } + + $roles = $attributes['roles'] ?? null; + $permissions = $attributes['permissions'] ?? null; + + unset($attributes['roles'], $attributes['permissions']); + + $updated = $user->fill($attributes)->save(); + + if (! $updated) { + DB::rollBack(); + return false; + } + + if (is_array($roles)) { + $user->syncRoles($roles); + } + + if (is_array($permissions)) { + $user->syncPermissions($permissions); + } + + DB::commit(); + + return true; + } catch (\Exception $e) { + DB::rollBack(); + Log::error('Error updating user with roles/permissions: ' . $e->getMessage(), [ + 'id' => $id, + 'attributes' => $attributes, + 'exception' => $e, + ]); + throw $e; + } + } +} diff --git a/thanasoft-back/app/Repositories/UserRepositoryInterface.php b/thanasoft-back/app/Repositories/UserRepositoryInterface.php new file mode 100644 index 0000000..0c3d3cf --- /dev/null +++ b/thanasoft-back/app/Repositories/UserRepositoryInterface.php @@ -0,0 +1,9 @@ +model->newQuery()->with(['primaryUser', 'convoys']); + + if (! empty($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('brand', 'like', '%' . $filters['search'] . '%') + ->orWhere('model', 'like', '%' . $filters['search'] . '%') + ->orWhere('registration_number', 'like', '%' . $filters['search'] . '%'); + }); + } + + if (! empty($filters['status'])) { + $query->where('status', $filters['status']); + } + + if (! empty($filters['vehicle_type'])) { + $query->where('vehicle_type', $filters['vehicle_type']); + } + + $sortField = $filters['sort_by'] ?? 'created_at'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + + return $query->orderBy($sortField, $sortDirection)->paginate($perPage); + } +} diff --git a/thanasoft-back/app/Repositories/VehicleRepositoryInterface.php b/thanasoft-back/app/Repositories/VehicleRepositoryInterface.php new file mode 100644 index 0000000..5008812 --- /dev/null +++ b/thanasoft-back/app/Repositories/VehicleRepositoryInterface.php @@ -0,0 +1,12 @@ +model->newQuery() + ->where('user_id', $userId) + ->orderByRaw('COALESCE(received_at, sent_at, created_at) DESC'); + + if (!empty($filters['folder'])) { + $query->where('folder', $filters['folder']); + } + + if (!empty($filters['status'])) { + $query->where('status', $filters['status']); + } + + if (array_key_exists('unread', $filters) && $filters['unread'] !== null) { + $filters['unread'] + ? $query->whereNull('read_at') + : $query->whereNotNull('read_at'); + } + + if (array_key_exists('starred', $filters) && $filters['starred'] !== null) { + $filters['starred'] + ? $query->whereNotNull('starred_at') + : $query->whereNull('starred_at'); + } + + if (!empty($filters['search'])) { + $search = '%' . trim((string) $filters['search']) . '%'; + + $query->where(function ($builder) use ($search): void { + $builder + ->where('subject', 'like', $search) + ->orWhere('from_email', 'like', $search) + ->orWhere('from_name', 'like', $search) + ->orWhere('body', 'like', $search) + ->orWhere('snippet', 'like', $search); + }); + } + + return $query->paginate($perPage); + } + + public function findForUser(int|string $id, int $userId): ?WebmailMessage + { + $message = $this->model->newQuery() + ->where('user_id', $userId) + ->find($id); + + return $message instanceof WebmailMessage ? $message : null; + } + + public function statsForUser(int $userId): array + { + $baseQuery = $this->model->newQuery()->where('user_id', $userId); + + return [ + 'total' => (clone $baseQuery)->count(), + 'inbox' => (clone $baseQuery)->where('folder', 'inbox')->count(), + 'sent' => (clone $baseQuery)->where('folder', 'sent')->count(), + 'drafts' => (clone $baseQuery)->where('folder', 'drafts')->count(), + 'trash' => (clone $baseQuery)->where('folder', 'trash')->count(), + 'unread' => (clone $baseQuery)->whereNull('read_at')->count(), + 'starred' => (clone $baseQuery)->whereNotNull('starred_at')->count(), + ]; + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Repositories/WebmailMessageRepositoryInterface.php b/thanasoft-back/app/Repositories/WebmailMessageRepositoryInterface.php new file mode 100644 index 0000000..1d73f89 --- /dev/null +++ b/thanasoft-back/app/Repositories/WebmailMessageRepositoryInterface.php @@ -0,0 +1,23 @@ + $filters + */ + public function paginateForUser(int $userId, array $filters = [], int $perPage = 15): LengthAwarePaginator; + + public function findForUser(int|string $id, int $userId): ?WebmailMessage; + + /** + * @return array + */ + public function statsForUser(int $userId): array; +} \ No newline at end of file diff --git a/thanasoft-back/app/Services/WebmailService.php b/thanasoft-back/app/Services/WebmailService.php new file mode 100644 index 0000000..f94fb78 --- /dev/null +++ b/thanasoft-back/app/Services/WebmailService.php @@ -0,0 +1,597 @@ + $payload + */ + public function send(array $payload, User $user): WebmailMessage + { + $fromEmail = $user->email ?: config('mail.from.address'); + $fromName = $user->name ?: config('mail.from.name'); + $mailboxSetting = $this->resolveMailboxSetting($user); + + $message = $this->webmailRepository->create([ + 'user_id' => $user->id, + 'message_uid' => $payload['message_uid'] ?? (string) Str::uuid(), + 'direction' => 'outgoing', + 'folder' => $payload['folder'] ?? 'sent', + 'status' => 'pending', + 'from_email' => $fromEmail, + 'from_name' => $fromName, + 'to_recipients' => $payload['to'], + 'cc_recipients' => $payload['cc'] ?? [], + 'bcc_recipients' => $payload['bcc'] ?? [], + 'subject' => $payload['subject'] ?? null, + 'body' => $payload['body'] ?? '', + 'snippet' => $this->makeSnippet($payload['body'] ?? ''), + 'attachments' => $payload['attachments'] ?? [], + 'metadata' => $payload['metadata'] ?? [], + ]); + + try { + if ($mailboxSetting instanceof UserMailboxSetting && $mailboxSetting->hasSmtpConfiguration()) { + $this->sendUsingUserSmtp($payload, $mailboxSetting, $fromEmail, $fromName); + } elseif ($mailboxSetting instanceof UserMailboxSetting) { + throw new \RuntimeException('La configuration SMTP utilisateur est incomplete. Renseignez smtp_host, smtp_port, smtp_username et smtp_password dans le parametrage emails.'); + } else { + $mailable = new WebmailMessageMail([ + 'subject' => $payload['subject'] ?? '', + 'body' => $payload['body'] ?? '', + ]); + + if (! empty($fromEmail)) { + $mailable->from($fromEmail, $fromName); + $mailable->replyTo($fromEmail, $fromName); + } + + $mailer = Mail::to($payload['to']); + + if (! empty($payload['cc'])) { + $mailer->cc($payload['cc']); + } + + if (! empty($payload['bcc'])) { + $mailer->bcc($payload['bcc']); + } + + $mailer->send($mailable); + } + + $this->webmailRepository->update($message->id, [ + 'status' => 'sent', + 'sent_at' => now(), + 'metadata' => array_merge(Arr::wrap($message->metadata), [ + 'transport' => $mailboxSetting instanceof UserMailboxSetting && $mailboxSetting->hasSmtpConfiguration() + ? 'user_smtp' + : 'app_mailer', + ]), + ]); + } catch (Throwable $throwable) { + $metadata = Arr::wrap($message->metadata); + $metadata['send_error'] = $throwable->getMessage(); + + $this->webmailRepository->update($message->id, [ + 'status' => 'failed', + 'metadata' => $metadata, + ]); + + throw $throwable; + } + + /** @var WebmailMessage $freshMessage */ + $freshMessage = $this->webmailRepository->find((string) $message->id); + + return $freshMessage; + } + + /** + * @return array{imported:int, skipped:int, source:string} + */ + public function syncMailbox(User $user, int $limit = 30): array + { + $mailboxSetting = $this->resolveMailboxSetting($user); + + if ($mailboxSetting instanceof UserMailboxSetting && $mailboxSetting->hasImapConfiguration()) { + return $this->syncImapInbox($user, $mailboxSetting, $limit); + } + + if ($mailboxSetting instanceof UserMailboxSetting) { + throw new \RuntimeException('La configuration IMAP utilisateur est incomplete. Renseignez imap_host, imap_port, imap_username et imap_password dans le parametrage emails.'); + } + + throw new \RuntimeException('Aucune configuration mailbox utilisateur n\'est enregistree. Configurez votre boite mail dans le parametrage emails.'); + } + + /** + * @return array{recipient:string, source:string} + */ + public function testSmtp(User $user): array + { + $mailboxSetting = $this->resolveMailboxSetting($user); + + if (! $mailboxSetting instanceof UserMailboxSetting) { + throw new \RuntimeException('Aucune configuration mailbox utilisateur n\'est enregistree. Configurez votre boite mail dans le parametrage emails.'); + } + + if (! $mailboxSetting->hasSmtpConfiguration()) { + throw new \RuntimeException('La configuration SMTP utilisateur est incomplete. Renseignez smtp_host, smtp_port, smtp_username et smtp_password dans le parametrage emails.'); + } + + $recipient = $this->resolveSmtpTestRecipient($user, $mailboxSetting); + $fromEmail = $user->email ?: config('mail.from.address'); + $fromName = $user->name ?: config('mail.from.name'); + $timestamp = now()->format('d/m/Y H:i:s'); + + $this->sendUsingUserSmtp([ + 'to' => [$recipient], + 'cc' => [], + 'bcc' => [], + 'subject' => 'Test SMTP Thanasoft', + 'body' => "Ceci est un email de test SMTP envoye depuis Thanasoft le {$timestamp}.", + ], $mailboxSetting, $fromEmail, $fromName); + + return [ + 'recipient' => $recipient, + 'source' => 'user_smtp', + ]; + } + + /** + * @return array{imported:int, skipped:int} + */ + public function syncMailtrapInbox(User $user, int $limit = 30): array + { + $accountId = (string) config('services.mailtrap.account_id'); + $inboxId = (string) config('services.mailtrap.inbox_id'); + $token = (string) config('services.mailtrap.token'); + $baseUrl = rtrim((string) config('services.mailtrap.base_url', 'https://mailtrap.io'), '/'); + + if ($accountId === '' || $inboxId === '' || $token === '') { + throw new \RuntimeException('La configuration Mailtrap est incomplete. Renseignez MAILTRAP_ACCOUNT_ID, MAILTRAP_INBOX_ID et MAILTRAP_API_TOKEN.'); + } + + if (empty($user->email)) { + throw new \RuntimeException('L\'utilisateur connecte ne possede pas d\'adresse email.'); + } + + $response = Http::baseUrl($baseUrl) + ->acceptJson() + ->withToken($token) + ->get("/api/accounts/{$accountId}/inboxes/{$inboxId}/messages"); + + $response->throw(); + + /** @var array> $messages */ + $messages = $response->json(); + $imported = 0; + $skipped = 0; + + foreach (array_slice($messages, 0, max(1, $limit)) as $sandboxMessage) { + if (! $this->messageTargetsUser($sandboxMessage, (string) $user->email)) { + $skipped++; + continue; + } + + $messageUid = 'mailtrap-' . (string) ($sandboxMessage['id'] ?? Str::uuid()); + + $alreadyExists = WebmailMessage::query() + ->where('user_id', $user->id) + ->where('message_uid', $messageUid) + ->exists(); + + if ($alreadyExists) { + $skipped++; + continue; + } + + $messageId = (string) ($sandboxMessage['id'] ?? ''); + $body = $messageId !== '' + ? $this->fetchMailtrapBody($baseUrl, $token, $accountId, $inboxId, $messageId) + : ''; + + $this->receive([ + 'message_uid' => $messageUid, + 'from_email' => $sandboxMessage['from_email'] ?? 'unknown@mailtrap.local', + 'from_name' => $sandboxMessage['from_name'] ?? null, + 'to' => $this->extractRecipients($sandboxMessage), + 'subject' => $sandboxMessage['subject'] ?? null, + 'body' => $body, + 'status' => 'received', + 'folder' => 'inbox', + 'received_at' => $sandboxMessage['sent_at'] ?? now()->toDateTimeString(), + 'metadata' => [ + 'provider' => 'mailtrap', + 'mailtrap_inbox_id' => $inboxId, + 'mailtrap_message_id' => $sandboxMessage['id'] ?? null, + 'mailtrap_raw' => $sandboxMessage, + ], + ], $user); + + $imported++; + } + + return [ + 'imported' => $imported, + 'skipped' => $skipped, + ]; + } + + /** + * @param array $payload + */ + public function receive(array $payload, User $user): WebmailMessage + { + $receivedAt = !empty($payload['received_at']) + ? Carbon::parse((string) $payload['received_at']) + : now(); + + /** @var WebmailMessage $message */ + $message = $this->webmailRepository->create([ + 'user_id' => $user->id, + 'message_uid' => $payload['message_uid'] ?? (string) Str::uuid(), + 'direction' => 'incoming', + 'folder' => $payload['folder'] ?? 'inbox', + 'status' => $payload['status'] ?? 'received', + 'from_email' => $payload['from_email'], + 'from_name' => $payload['from_name'] ?? null, + 'to_recipients' => $payload['to'], + 'cc_recipients' => $payload['cc'] ?? [], + 'bcc_recipients' => $payload['bcc'] ?? [], + 'subject' => $payload['subject'] ?? null, + 'body' => $payload['body'] ?? '', + 'snippet' => $this->makeSnippet($payload['body'] ?? ''), + 'attachments' => $payload['attachments'] ?? [], + 'metadata' => $payload['metadata'] ?? [], + 'received_at' => $receivedAt, + ]); + + return $message; + } + + private function makeSnippet(string $body): string + { + return Str::limit(trim(strip_tags($body)), 160, '...'); + } + + private function resolveMailboxSetting(User $user): ?UserMailboxSetting + { + if ($user->relationLoaded('mailboxSetting')) { + $mailboxSetting = $user->mailboxSetting; + + return $mailboxSetting instanceof UserMailboxSetting ? $mailboxSetting : null; + } + + $mailboxSetting = $user->mailboxSetting()->first(); + + return $mailboxSetting instanceof UserMailboxSetting ? $mailboxSetting : null; + } + + private function resolveSmtpTestRecipient(User $user, UserMailboxSetting $mailboxSetting): string + { + $candidates = [ + $mailboxSetting->smtp_from_address, + $user->email, + $mailboxSetting->smtp_username, + ]; + + foreach ($candidates as $candidate) { + $email = is_string($candidate) ? trim($candidate) : ''; + + if ($email !== '') { + return $email; + } + } + + throw new \RuntimeException('Impossible de determiner l\'adresse de destination du test SMTP. Renseignez smtp_from_address ou l\'email utilisateur.'); + } + + private function sendUsingUserSmtp( + array $payload, + UserMailboxSetting $mailboxSetting, + string $fallbackFromEmail, + ?string $fallbackFromName, + ): void { + $transport = Transport::fromDsn($this->buildSmtpDsn($mailboxSetting)); + $mailer = new Mailer($transport); + + $fromEmail = $mailboxSetting->smtp_from_address ?: $fallbackFromEmail; + $fromName = $mailboxSetting->smtp_from_name ?: $fallbackFromName; + $htmlBody = view('emails.webmail_message', [ + 'body' => $payload['body'] ?? '', + ])->render(); + + $email = (new Email()) + ->from(new Address($fromEmail, $fromName ?? '')) + ->replyTo(new Address($fromEmail, $fromName ?? '')) + ->subject((string) ($payload['subject'] ?? '')) + ->html($htmlBody) + ->text(trim(strip_tags((string) ($payload['body'] ?? '')))); + + foreach ($payload['to'] as $recipient) { + $email->addTo(new Address((string) $recipient)); + } + + foreach ($payload['cc'] ?? [] as $recipient) { + $email->addCc(new Address((string) $recipient)); + } + + foreach ($payload['bcc'] ?? [] as $recipient) { + $email->addBcc(new Address((string) $recipient)); + } + + $mailer->send($email); + } + + /** + * @return array{imported:int, skipped:int, source:string} + */ + private function syncImapInbox(User $user, UserMailboxSetting $mailboxSetting, int $limit): array + { + $imported = 0; + $skipped = 0; + $syncedSince = $mailboxSetting->last_synced_at?->copy()->subDay(); + + try { + $clientManager = new ClientManager([ + 'options' => [ + 'fetch' => null, + ], + 'accounts' => [ + 'default' => [ + 'host' => $mailboxSetting->imap_host, + 'port' => $mailboxSetting->imap_port, + 'encryption' => $this->normalizeImapEncryption($mailboxSetting->imap_encryption), + 'validate_cert' => $mailboxSetting->imap_validate_cert, + 'username' => $mailboxSetting->imap_username, + 'password' => $mailboxSetting->imap_password, + 'protocol' => 'imap', + ], + ], + ]); + + $client = $clientManager->account('default'); + $client->connect(); + + $folder = $client->getFolder($mailboxSetting->imap_folder ?: 'INBOX'); + $query = $folder->query() + ->all() + ->setFetchOrderDesc() + ->limit(max(1, $limit)); + + /** @var iterable $messages */ + $messages = $query->get(); + + foreach ($messages as $imapMessage) { + $messageDate = $imapMessage->getDate()?->toDate(); + + if ($syncedSince && $messageDate && $messageDate->lt($syncedSince)) { + $skipped++; + continue; + } + + $messageUid = 'imap-' . (string) $imapMessage->getUid(); + + $alreadyExists = WebmailMessage::query() + ->where('user_id', $user->id) + ->where('message_uid', $messageUid) + ->exists(); + + if ($alreadyExists) { + $skipped++; + continue; + } + + $body = trim($imapMessage->getTextBody()); + if ($body === '') { + $body = trim(strip_tags($imapMessage->getHTMLBody())); + } + + $from = $this->extractAddressList($imapMessage->getFrom()); + $to = $this->extractAddressList($imapMessage->getTo()); + $cc = $this->extractAddressList($imapMessage->getCc()); + $bcc = $this->extractAddressList($imapMessage->getBcc()); + + $this->receive([ + 'message_uid' => $messageUid, + 'from_email' => $from[0]['email'] ?? 'unknown@imap.local', + 'from_name' => $from[0]['name'] ?? null, + 'to' => array_values(array_filter(array_map(fn (array $item): ?string => $item['email'] ?? null, $to))), + 'cc' => array_values(array_filter(array_map(fn (array $item): ?string => $item['email'] ?? null, $cc))), + 'bcc' => array_values(array_filter(array_map(fn (array $item): ?string => $item['email'] ?? null, $bcc))), + 'subject' => (string) $imapMessage->getSubject(), + 'body' => $body, + 'status' => 'received', + 'folder' => 'inbox', + 'received_at' => ($messageDate ?? now())->toDateTimeString(), + 'attachments' => [], + 'metadata' => [ + 'provider' => 'imap', + 'imap_uid' => (string) $imapMessage->getUid(), + ], + ], $user); + + $imported++; + } + + $mailboxSetting->forceFill([ + 'last_synced_at' => now(), + 'last_sync_error' => null, + ])->save(); + + $client->disconnect(); + } catch (Throwable $throwable) { + $mailboxSetting->forceFill([ + 'last_sync_error' => $throwable->getMessage(), + ])->save(); + + throw $throwable; + } + + return [ + 'imported' => $imported, + 'skipped' => $skipped, + 'source' => 'imap', + ]; + } + + private function buildSmtpDsn(UserMailboxSetting $mailboxSetting): string + { + $scheme = ($mailboxSetting->smtp_encryption ?? '') === 'ssl' ? 'smtps' : 'smtp'; + $username = rawurlencode((string) $mailboxSetting->smtp_username); + $password = rawurlencode((string) $mailboxSetting->smtp_password); + $host = (string) $mailboxSetting->smtp_host; + $port = (int) $mailboxSetting->smtp_port; + $query = []; + + if (($mailboxSetting->smtp_encryption ?? '') === 'tls') { + $query['encryption'] = 'tls'; + } + + if (! $mailboxSetting->smtp_validate_cert) { + $query['verify_peer'] = '0'; + } + + $suffix = $query === [] ? '' : '?' . http_build_query($query); + + return sprintf('%s://%s:%s@%s:%d%s', $scheme, $username, $password, $host, $port, $suffix); + } + + private function normalizeImapEncryption(?string $encryption): ?string + { + if ($encryption === null || $encryption === '' || $encryption === 'none') { + return null; + } + + return $encryption; + } + + /** + * @return array + */ + private function extractAddressList(mixed $attribute): array + { + if (! $attribute instanceof Attribute) { + return []; + } + + return collect($attribute->toArray()) + ->map(function (mixed $item): array { + if (is_object($item)) { + return [ + 'name' => isset($item->personal) ? (string) $item->personal : null, + 'email' => isset($item->mail) ? (string) $item->mail : null, + ]; + } + + if (is_array($item)) { + return [ + 'name' => isset($item['personal']) ? (string) $item['personal'] : null, + 'email' => isset($item['mail']) ? (string) $item['mail'] : null, + ]; + } + + return [ + 'name' => null, + 'email' => is_string($item) ? $item : null, + ]; + }) + ->filter(fn (array $item): bool => filled($item['email'])) + ->values() + ->all(); + } + + /** + * @param array $sandboxMessage + */ + private function messageTargetsUser(array $sandboxMessage, string $userEmail): bool + { + $userEmail = Str::lower(trim($userEmail)); + + return collect($this->extractRecipients($sandboxMessage)) + ->map(fn (mixed $email): string => Str::lower(trim((string) $email))) + ->contains($userEmail); + } + + /** + * @param array $sandboxMessage + * @return array + */ + private function extractRecipients(array $sandboxMessage): array + { + $candidates = [ + $sandboxMessage['to'] ?? null, + $sandboxMessage['to_email'] ?? null, + $sandboxMessage['to_emails'] ?? null, + ]; + + return collect($candidates) + ->flatten(1) + ->map(function (mixed $recipient): ?string { + if (is_array($recipient)) { + $email = $recipient['email'] ?? $recipient['address'] ?? $recipient['to_email'] ?? null; + return is_string($email) ? trim($email) : null; + } + + return is_string($recipient) ? trim($recipient) : null; + }) + ->filter(fn (?string $email): bool => ! empty($email)) + ->unique() + ->values() + ->all(); + } + + private function fetchMailtrapBody(string $baseUrl, string $token, string $accountId, string $inboxId, string $messageId): string + { + $textResponse = Http::baseUrl($baseUrl) + ->withToken($token) + ->accept('text/plain') + ->get("/api/accounts/{$accountId}/inboxes/{$inboxId}/messages/{$messageId}/body.txt"); + + if ($textResponse->successful()) { + $body = trim($textResponse->body()); + if ($body !== '') { + return $body; + } + } + + $htmlResponse = Http::baseUrl($baseUrl) + ->withToken($token) + ->accept('text/html') + ->get("/api/accounts/{$accountId}/inboxes/{$inboxId}/messages/{$messageId}/body.html"); + + if (! $htmlResponse->successful()) { + return ''; + } + + return trim(strip_tags($htmlResponse->body())); + } +} \ No newline at end of file diff --git a/thanasoft-back/bootstrap/app.php b/thanasoft-back/bootstrap/app.php index c3928c5..020402e 100644 --- a/thanasoft-back/bootstrap/app.php +++ b/thanasoft-back/bootstrap/app.php @@ -3,6 +3,9 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; +use Spatie\Permission\Middleware\PermissionMiddleware; +use Spatie\Permission\Middleware\RoleMiddleware; +use Spatie\Permission\Middleware\RoleOrPermissionMiddleware; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( @@ -12,7 +15,12 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - // + $middleware->statefulApi(); + $middleware->alias([ + 'role' => RoleMiddleware::class, + 'permission' => PermissionMiddleware::class, + 'role_or_permission' => RoleOrPermissionMiddleware::class, + ]); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/thanasoft-back/bootstrap/providers.php b/thanasoft-back/bootstrap/providers.php index 38b258d..3734166 100644 --- a/thanasoft-back/bootstrap/providers.php +++ b/thanasoft-back/bootstrap/providers.php @@ -2,4 +2,5 @@ return [ App\Providers\AppServiceProvider::class, + App\Providers\RepositoryServiceProvider::class, ]; diff --git a/thanasoft-back/composer.json b/thanasoft-back/composer.json index 34284d9..2f0f5b7 100644 --- a/thanasoft-back/composer.json +++ b/thanasoft-back/composer.json @@ -10,9 +10,12 @@ "license": "MIT", "require": { "php": "^8.2", + "barryvdh/laravel-dompdf": "^3.1", "laravel/framework": "^12.0", "laravel/sanctum": "^4.2", - "laravel/tinker": "^2.10.1" + "laravel/tinker": "^2.10.1", + "spatie/laravel-permission": "^6.18", + "webklex/php-imap": "^6.2" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/thanasoft-back/composer.lock b/thanasoft-back/composer.lock index 3089aab..9f8af1e 100644 --- a/thanasoft-back/composer.lock +++ b/thanasoft-back/composer.lock @@ -4,8 +4,85 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8f387a0734f3bf879214e4aa2fca6e2f", + "content-hash": "74fc1ffaa567d424ef63bdd4f9dea808", "packages": [ + { + "name": "barryvdh/laravel-dompdf", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-dompdf.git", + "reference": "8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d", + "reference": "8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d", + "shasum": "" + }, + "require": { + "dompdf/dompdf": "^3.0", + "illuminate/support": "^9|^10|^11|^12", + "php": "^8.1" + }, + "require-dev": { + "larastan/larastan": "^2.7|^3.0", + "orchestra/testbench": "^7|^8|^9|^10", + "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.1" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-02-13T15:07:54+00:00" + }, { "name": "brick/math", "version": "0.14.0", @@ -377,6 +454,161 @@ ], "time": "2024-02-05T11:56:58+00:00" }, + { + "name": "dompdf/dompdf", + "version": "v3.1.4", + "source": { + "type": "git", + "url": "https://github.com/dompdf/dompdf.git", + "reference": "db712c90c5b9868df3600e64e68da62e78a34623" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/db712c90c5b9868df3600e64e68da62e78a34623", + "reference": "db712c90c5b9868df3600e64e68da62e78a34623", + "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.4" + }, + "time": "2025-10-29T12:43:30+00:00" + }, + { + "name": "dompdf/php-font-lib", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-font-lib.git", + "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", + "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6" + }, + "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.1" + }, + "time": "2024-12-02T14:37:59+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", "version": "v3.4.0", @@ -2074,6 +2306,73 @@ ], "time": "2024-12-08T08:18:47+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", "version": "3.9.0", @@ -3412,6 +3711,164 @@ }, "time": "2025-09-04T20:59:21+00:00" }, + { + "name": "sabberworm/php-css-parser", + "version": "v9.1.0", + "source": { + "type": "git", + "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", + "reference": "1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb", + "reference": "1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb", + "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.3" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/extension-installer": "1.4.3", + "phpstan/phpstan": "1.12.28 || 2.1.25", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.7", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.6", + "phpunit/phpunit": "8.5.46", + "rawr/phpunit-data-provider": "3.3.1", + "rector/rector": "1.2.10 || 2.1.7", + "rector/type-perfect": "1.0.0 || 2.1.0" + }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.2.x-dev" + } + }, + "autoload": { + "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.1.0" + }, + "time": "2025-09-14T07:37:21+00:00" + }, + { + "name": "spatie/laravel-permission", + "version": "6.25.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-permission.git", + "reference": "d7d4cb0d58616722f1afc90e0484e4825155b9b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/d7d4cb0d58616722f1afc90e0484e4825155b9b3", + "reference": "d7d4cb0d58616722f1afc90e0484e4825155b9b3", + "shasum": "" + }, + "require": { + "illuminate/auth": "^8.12|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/container": "^8.12|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^8.12|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/database": "^8.12|^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0" + }, + "require-dev": { + "laravel/passport": "^11.0|^12.0|^13.0", + "laravel/pint": "^1.0", + "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0|^11.0", + "pestphp/pest": "^2.0|^3.0|^4.0", + "pestphp/pest-plugin-laravel": "^2.0|^3.0|^4.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Permission\\PermissionServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "6.x-dev", + "dev-master": "6.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Permission\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Permission handling for Laravel 8.0 and up", + "homepage": "https://github.com/spatie/laravel-permission", + "keywords": [ + "acl", + "laravel", + "permission", + "permissions", + "rbac", + "roles", + "security", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-permission/issues", + "source": "https://github.com/spatie/laravel-permission/tree/6.25.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-03-17T22:46:46+00:00" + }, { "name": "symfony/clock", "version": "v7.3.0", @@ -5890,6 +6347,145 @@ ], "time": "2025-08-13T11:49:31+00:00" }, + { + "name": "thecodingmachine/safe", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/2cdd579eeaa2e78e51c7509b50cc9fb89a956236", + "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236", + "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.3.0" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2025-05-14T06:15:44+00:00" + }, { "name": "tijsverkoyen/css-to-inline-styles", "version": "v2.3.0", @@ -6103,6 +6699,87 @@ ], "time": "2024-11-21T01:49:47+00:00" }, + { + "name": "webklex/php-imap", + "version": "6.2.0", + "source": { + "type": "git", + "url": "https://github.com/Webklex/php-imap.git", + "reference": "6b8ef85d621bbbaf52741b00cca8e9237e2b2e05" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Webklex/php-imap/zipball/6b8ef85d621bbbaf52741b00cca8e9237e2b2e05", + "reference": "6b8ef85d621bbbaf52741b00cca8e9237e2b2e05", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "ext-iconv": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-zip": "*", + "illuminate/pagination": ">=5.0.0", + "nesbot/carbon": "^2.62.1|^3.2.4", + "php": "^8.0.2", + "symfony/http-foundation": ">=2.8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5.10" + }, + "suggest": { + "symfony/mime": "Recomended for better extension support", + "symfony/var-dumper": "Usefull tool for debugging" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webklex\\PHPIMAP\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Malte Goldenbaum", + "email": "github@webklex.com", + "role": "Developer" + } + ], + "description": "PHP IMAP client", + "homepage": "https://github.com/webklex/php-imap", + "keywords": [ + "imap", + "mail", + "php-imap", + "pop3", + "webklex" + ], + "support": { + "issues": "https://github.com/Webklex/php-imap/issues", + "source": "https://github.com/Webklex/php-imap/tree/6.2.0" + }, + "funding": [ + { + "url": "https://www.buymeacoffee.com/webklex", + "type": "custom" + }, + { + "url": "https://ko-fi.com/webklex", + "type": "ko_fi" + } + ], + "time": "2025-04-25T06:02:37+00:00" + }, { "name": "webmozart/assert", "version": "1.11.0", diff --git a/thanasoft-back/config/auth.php b/thanasoft-back/config/auth.php index 7d1eb0d..b774d91 100644 --- a/thanasoft-back/config/auth.php +++ b/thanasoft-back/config/auth.php @@ -40,6 +40,11 @@ return [ 'driver' => 'session', 'provider' => 'users', ], + + 'sanctum' => [ + 'driver' => 'sanctum', + 'provider' => 'users', + ], ], /* diff --git a/thanasoft-back/config/cors.php b/thanasoft-back/config/cors.php index 97f424a..e4a281d 100644 --- a/thanasoft-back/config/cors.php +++ b/thanasoft-back/config/cors.php @@ -9,7 +9,7 @@ return [ // IMPORTANT: Do NOT use '*' when sending credentials. List explicit origins. // Set FRONTEND_URL in .env to override the default if needed. - 'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:8080')], + 'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:8081'), 'http://localhost:8080'], // Alternatively, use patterns (kept empty for clarity) 'allowed_origins_patterns' => [], diff --git a/thanasoft-back/config/permission.php b/thanasoft-back/config/permission.php new file mode 100644 index 0000000..85b8832 --- /dev/null +++ b/thanasoft-back/config/permission.php @@ -0,0 +1,206 @@ + [ + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * Eloquent model should be used to retrieve your permissions. Of course, it + * is often just the "Permission" model but you may use whatever you like. + * + * The model you want to use as a Permission model needs to implement the + * `Spatie\Permission\Contracts\Permission` contract. + */ + + 'permission' => Permission::class, + + /* + * When using the "HasRoles" trait from this package, we need to know which + * Eloquent model should be used to retrieve your roles. Of course, it + * is often just the "Role" model but you may use whatever you like. + * + * The model you want to use as a Role model needs to implement the + * `Spatie\Permission\Contracts\Role` contract. + */ + + 'role' => Role::class, + + ], + + 'table_names' => [ + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'roles' => 'roles', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your permissions. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'permissions' => 'permissions', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your models permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_permissions' => 'model_has_permissions', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your models roles. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_roles' => 'model_has_roles', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'role_has_permissions' => 'role_has_permissions', + ], + + 'column_names' => [ + /* + * Change this if you want to name the related pivots other than defaults + */ + 'role_pivot_key' => null, // default 'role_id', + 'permission_pivot_key' => null, // default 'permission_id', + + /* + * Change this if you want to name the related model primary key other than + * `model_id`. + * + * For example, this would be nice if your primary keys are all UUIDs. In + * that case, name this `model_uuid`. + */ + + 'model_morph_key' => 'model_id', + + /* + * Change this if you want to use the teams feature and your related model's + * foreign key is other than `team_id`. + */ + + 'team_foreign_key' => 'team_id', + ], + + /* + * When set to true, the method for checking permissions will be registered on the gate. + * Set this to false if you want to implement custom logic for checking permissions. + */ + + 'register_permission_check_method' => true, + + /* + * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered + * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated + * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it. + */ + 'register_octane_reset_listener' => false, + + /* + * Events will fire when a role or permission is assigned/unassigned: + * \Spatie\Permission\Events\RoleAttached + * \Spatie\Permission\Events\RoleDetached + * \Spatie\Permission\Events\PermissionAttached + * \Spatie\Permission\Events\PermissionDetached + * + * To enable, set to true, and then create listeners to watch these events. + */ + 'events_enabled' => false, + + /* + * Teams Feature. + * When set to true the package implements teams using the 'team_foreign_key'. + * If you want the migrations to register the 'team_foreign_key', you must + * set this to true before doing the migration. + * If you already did the migration then you must make a new migration to also + * add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions' + * (view the latest version of this package's migration file) + */ + + 'teams' => false, + + /* + * The class to use to resolve the permissions team id + */ + 'team_resolver' => DefaultTeamResolver::class, + + /* + * Passport Client Credentials Grant + * When set to true the package will use Passports Client to check permissions + */ + + 'use_passport_client_credentials' => false, + + /* + * When set to true, the required permission names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_permission_in_exception' => false, + + /* + * When set to true, the required role names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_role_in_exception' => false, + + /* + * By default wildcard permission lookups are disabled. + * See documentation to understand supported syntax. + */ + + 'enable_wildcard_permission' => false, + + /* + * The class to use for interpreting wildcard permissions. + * If you need to modify delimiters, override the class and specify its name here. + */ + // 'wildcard_permission' => Spatie\Permission\WildcardPermission::class, + + /* Cache-specific settings */ + + 'cache' => [ + + /* + * By default all permissions are cached for 24 hours to speed up performance. + * When permissions or roles are updated the cache is flushed automatically. + */ + + 'expiration_time' => DateInterval::createFromDateString('24 hours'), + + /* + * The cache key used to store all permissions. + */ + + 'key' => 'spatie.permission.cache', + + /* + * You may optionally indicate a specific cache driver to use for permission and + * role caching using any of the `store` drivers listed in the cache.php config + * file. Using 'default' here means to use the `default` set in cache.php. + */ + + 'store' => 'default', + ], +]; diff --git a/thanasoft-back/config/services.php b/thanasoft-back/config/services.php index 6182e4b..a2d46c1 100644 --- a/thanasoft-back/config/services.php +++ b/thanasoft-back/config/services.php @@ -35,4 +35,11 @@ return [ ], ], + 'mailtrap' => [ + 'base_url' => env('MAILTRAP_BASE_URL', 'https://mailtrap.io'), + 'account_id' => env('MAILTRAP_ACCOUNT_ID'), + 'inbox_id' => env('MAILTRAP_INBOX_ID'), + 'token' => env('MAILTRAP_API_TOKEN'), + ], + ]; diff --git a/thanasoft-back/database/factories/ClientFactory.php b/thanasoft-back/database/factories/ClientFactory.php new file mode 100644 index 0000000..d100d39 --- /dev/null +++ b/thanasoft-back/database/factories/ClientFactory.php @@ -0,0 +1,31 @@ + $this->faker->company(), + 'vat_number' => 'FR' . $this->faker->numerify('###########'), + 'siret' => $this->faker->numerify('##############'), + 'email' => $this->faker->unique()->companyEmail(), + 'phone' => $this->faker->phoneNumber(), + 'billing_address_line1' => $this->faker->streetAddress(), + 'billing_city' => $this->faker->city(), + 'billing_postal_code' => $this->faker->postcode(), + 'billing_country_code' => 'FR', + // Assumes categories 1-5 exist from migration + 'client_category_id' => $this->faker->numberBetween(1, 5), + 'is_active' => true, + 'is_parent' => false, + 'notes' => $this->faker->optional()->sentence(), + ]; + } +} diff --git a/thanasoft-back/database/factories/ProductPackagingFactory.php b/thanasoft-back/database/factories/ProductPackagingFactory.php new file mode 100644 index 0000000..500a546 --- /dev/null +++ b/thanasoft-back/database/factories/ProductPackagingFactory.php @@ -0,0 +1,29 @@ + + */ +class ProductPackagingFactory extends Factory +{ + protected $model = ProductPackaging::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'product_id' => Product::factory(), + 'name' => $this->faker->word(), + 'qty_base' => $this->faker->randomFloat(3, 1, 100), + ]; + } +} diff --git a/thanasoft-back/database/factories/StockItemFactory.php b/thanasoft-back/database/factories/StockItemFactory.php new file mode 100644 index 0000000..eb46adc --- /dev/null +++ b/thanasoft-back/database/factories/StockItemFactory.php @@ -0,0 +1,31 @@ + + */ +class StockItemFactory extends Factory +{ + protected $model = StockItem::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'product_id' => Product::factory(), + 'warehouse_id' => Warehouse::factory(), + 'qty_on_hand_base' => $this->faker->randomFloat(3, 0, 1000), + 'safety_stock_base' => $this->faker->randomFloat(3, 0, 100), + ]; + } +} diff --git a/thanasoft-back/database/factories/StockMoveFactory.php b/thanasoft-back/database/factories/StockMoveFactory.php new file mode 100644 index 0000000..103f2a9 --- /dev/null +++ b/thanasoft-back/database/factories/StockMoveFactory.php @@ -0,0 +1,33 @@ + + */ +class StockMoveFactory extends Factory +{ + protected $model = StockMove::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'product_id' => Product::factory(), + 'from_warehouse_id' => Warehouse::factory(), + 'to_warehouse_id' => Warehouse::factory(), + 'qty_base' => $this->faker->randomFloat(3, 1, 100), + 'move_type' => $this->faker->randomElement(['receipt', 'issue', 'transfer', 'adjustment', 'consumption']), + 'moved_at' => now(), + ]; + } +} diff --git a/thanasoft-back/database/factories/WarehouseFactory.php b/thanasoft-back/database/factories/WarehouseFactory.php new file mode 100644 index 0000000..5eaeb2d --- /dev/null +++ b/thanasoft-back/database/factories/WarehouseFactory.php @@ -0,0 +1,31 @@ + + */ +class WarehouseFactory extends Factory +{ + protected $model = Warehouse::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->company() . ' Warehouse', + 'address_line1' => $this->faker->streetAddress(), + 'address_line2' => $this->faker->secondaryAddress(), + 'postal_code' => $this->faker->postcode(), + 'city' => $this->faker->city(), + 'country_code' => 'FR', + ]; + } +} diff --git a/thanasoft-back/database/migrations/2025_10_06_145345_create_personal_access_tokens_table.php b/thanasoft-back/database/migrations/2025_10_06_145345_create_personal_access_tokens_table.php deleted file mode 100644 index 40ff706..0000000 --- a/thanasoft-back/database/migrations/2025_10_06_145345_create_personal_access_tokens_table.php +++ /dev/null @@ -1,33 +0,0 @@ -id(); - $table->morphs('tokenable'); - $table->text('name'); - $table->string('token', 64)->unique(); - $table->text('abilities')->nullable(); - $table->timestamp('last_used_at')->nullable(); - $table->timestamp('expires_at')->nullable()->index(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('personal_access_tokens'); - } -}; diff --git a/thanasoft-back/database/migrations/2025_10_07_113450_create_client_groups_table.php b/thanasoft-back/database/migrations/2025_10_07_113450_create_client_groups_table.php new file mode 100644 index 0000000..4c7b073 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_10_07_113450_create_client_groups_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('name', 191)->unique(); + $table->text('description')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('client_groups'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_10_07_113522_create_clients_table.php b/thanasoft-back/database/migrations/2025_10_07_113522_create_clients_table.php new file mode 100644 index 0000000..5fd7495 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_10_07_113522_create_clients_table.php @@ -0,0 +1,50 @@ +id(); + $table->enum('type', [ + 'pompes_funebres', + 'famille', + 'entreprise', + 'collectivite', + 'autre' + ])->default('pompes_funebres'); + $table->string('name', 255); + $table->string('vat_number', 32)->nullable(); + $table->string('siret', 20)->nullable(); + $table->string('email', 191)->nullable(); + $table->string('phone', 50)->nullable(); + $table->string('billing_address_line1', 255)->nullable(); + $table->string('billing_address_line2', 255)->nullable(); + $table->string('billing_postal_code', 20)->nullable(); + $table->string('billing_city', 191)->nullable(); + $table->char('billing_country_code', 2)->default('FR'); + $table->foreignId('group_id')->nullable()->constrained('client_groups')->onDelete('set null'); + $table->text('notes')->nullable(); + $table->boolean('is_active')->default(true); + //$table->foreignId('default_tva_rate_id')->nullable()->constrained('tva_rates')->onDelete('set null'); + $table->timestamps(); + + $table->index(['group_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('clients'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_10_07_113551_create_contacts_table.php b/thanasoft-back/database/migrations/2025_10_07_113551_create_contacts_table.php new file mode 100644 index 0000000..6e3fe7e --- /dev/null +++ b/thanasoft-back/database/migrations/2025_10_07_113551_create_contacts_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('client_id')->constrained()->onDelete('cascade'); + $table->string('first_name', 191)->nullable(); + $table->string('last_name', 191)->nullable(); + $table->string('email', 191)->nullable(); + $table->string('phone', 50)->nullable(); + $table->string('role', 191)->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('contacts'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_10_07_113647_create_client_locations_table.php b/thanasoft-back/database/migrations/2025_10_07_113647_create_client_locations_table.php new file mode 100644 index 0000000..dc54f12 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_10_07_113647_create_client_locations_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('client_id')->constrained()->onDelete('cascade'); + $table->string('name', 191)->nullable(); + $table->string('address_line1', 255)->nullable(); + $table->string('address_line2', 255)->nullable(); + $table->string('postal_code', 20)->nullable(); + $table->string('city', 191)->nullable(); + $table->char('country_code', 2)->default('FR'); + $table->double('gps_lat')->nullable(); + $table->double('gps_lng')->nullable(); + $table->boolean('is_default')->default(false); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('client_locations'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_10_09_065542_create_table_client_categories.php b/thanasoft-back/database/migrations/2025_10_09_065542_create_table_client_categories.php new file mode 100644 index 0000000..2affb13 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_10_09_065542_create_table_client_categories.php @@ -0,0 +1,126 @@ +id(); + $table->string('name', 255); + $table->string('slug', 191)->unique(); + $table->text('description')->nullable(); + $table->boolean('is_active')->default(true); + $table->integer('sort_order')->default(0); + $table->timestamps(); + + $table->index(['slug', 'is_active']); + }); + + // Insert default categories + DB::table('client_categories')->insert([ + [ + 'name' => 'Pompes funèbres', + 'slug' => 'pompes_funebres', + 'description' => 'Professionnels des pompes funèbres', + 'sort_order' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'Famille', + 'slug' => 'famille', + 'description' => 'Particuliers et familles', + 'sort_order' => 2, + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'Entreprise', + 'slug' => 'entreprise', + 'description' => 'Entreprises et sociétés', + 'sort_order' => 3, + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'Collectivité', + 'slug' => 'collectivite', + 'description' => 'Collectivités territoriales', + 'sort_order' => 4, + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'Autre', + 'slug' => 'autre', + 'description' => 'Autres types de clients', + 'sort_order' => 5, + 'created_at' => now(), + 'updated_at' => now(), + ], + ]); + + // Add client_category_id to clients table + Schema::table('clients', function (Blueprint $table) { + $table->foreignId('client_category_id')->nullable()->after('type') + ->constrained('client_categories')->onDelete('set null'); + }); + + // Migrate existing type data to new client_category_id + $categories = DB::table('client_categories')->pluck('id', 'slug'); + + foreach ($categories as $slug => $id) { + DB::table('clients') + ->where('type', $slug) + ->update(['client_category_id' => $id]); + } + + // Remove the old type column + Schema::table('clients', function (Blueprint $table) { + $table->dropColumn('type'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Add back the type column + Schema::table('clients', function (Blueprint $table) { + $table->enum('type', [ + 'pompes_funebres', + 'famille', + 'entreprise', + 'collectivite', + 'autre' + ])->default('pompes_funebres')->after('id'); + }); + + // Migrate data back to type column + $categories = DB::table('client_categories')->pluck('slug', 'id'); + + foreach ($categories as $id => $slug) { + DB::table('clients') + ->where('client_category_id', $id) + ->update(['type' => $slug]); + } + + // Remove client_category_id + Schema::table('clients', function (Blueprint $table) { + $table->dropForeign(['client_category_id']); + $table->dropColumn('client_category_id'); + }); + + // Drop client_categories table + Schema::dropIfExists('client_categories'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_10_09_074317_add_user_id_tables.php b/thanasoft-back/database/migrations/2025_10_09_074317_add_user_id_tables.php new file mode 100644 index 0000000..462c5a5 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_10_09_074317_add_user_id_tables.php @@ -0,0 +1,32 @@ +foreignId('user_id')->nullable()->after('id') + ->constrained('users')->onDelete('set null'); + + $table->index(['user_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('clients', function (Blueprint $table) { + $table->dropForeign(['user_id']); + $table->dropColumn('user_id'); + }); + } +}; diff --git a/thanasoft-back/database/migrations/2025_10_28_131100_create_fournisseurs_table.php b/thanasoft-back/database/migrations/2025_10_28_131100_create_fournisseurs_table.php new file mode 100644 index 0000000..3b6d21a --- /dev/null +++ b/thanasoft-back/database/migrations/2025_10_28_131100_create_fournisseurs_table.php @@ -0,0 +1,40 @@ +id(); + $table->string('name'); + $table->string('vat_number', 32)->nullable(); + $table->string('siret', 20)->nullable(); + $table->string('email', 191)->nullable(); + $table->string('phone', 50)->nullable(); + $table->string('billing_address_line1')->nullable(); + $table->string('billing_address_line2')->nullable(); + $table->string('billing_postal_code', 20)->nullable(); + $table->string('billing_city', 191)->nullable(); + $table->string('billing_country_code', 2)->nullable(); + $table->text('notes')->nullable(); + $table->boolean('is_active')->default(true); + $table->foreignId('user_id')->nullable()->constrained()->onDelete('set null'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('fournisseurs'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_10_29_103700_add_fournisseur_to_contacts_table.php b/thanasoft-back/database/migrations/2025_10_29_103700_add_fournisseur_to_contacts_table.php new file mode 100644 index 0000000..52dd402 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_10_29_103700_add_fournisseur_to_contacts_table.php @@ -0,0 +1,41 @@ +dropForeign(['client_id']); + $table->foreignId('client_id')->nullable()->change(); + $table->foreign('client_id')->references('id')->on('clients')->onDelete('set null'); + + // Add fournisseur_id + $table->foreignId('fournisseur_id')->nullable()->after('client_id')->constrained('fournisseurs')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('contacts', function (Blueprint $table) { + // Remove fournisseur_id + $table->dropForeign(['fournisseur_id']); + $table->dropColumn('fournisseur_id'); + + // Restore client_id to not nullable with cascade + $table->dropForeign(['client_id']); + $table->foreignId('client_id')->change(); + $table->foreign('client_id')->references('id')->on('clients')->onDelete('cascade'); + }); + } +}; diff --git a/thanasoft-back/database/migrations/2025_10_31_091822_create_products_table.php b/thanasoft-back/database/migrations/2025_10_31_091822_create_products_table.php new file mode 100644 index 0000000..b1e5509 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_10_31_091822_create_products_table.php @@ -0,0 +1,43 @@ +id(); + $table->string('nom'); + $table->string('reference')->nullable(); + $table->string('categorie'); + $table->string('fabricant')->nullable(); + $table->decimal('stock_actuel', 10, 2); + $table->decimal('stock_minimum', 10, 2)->nullable(); + $table->string('unite'); + $table->decimal('prix_unitaire', 10, 2)->nullable(); + $table->date('date_expiration')->nullable(); + $table->string('numero_lot')->nullable(); + $table->string('conditionnement_nom')->nullable(); + $table->decimal('conditionnement_quantite', 10, 2)->nullable(); + $table->string('conditionnement_unite')->nullable(); + $table->string('photo_url')->nullable(); + $table->string('fiche_technique_url')->nullable(); + $table->foreignId('fournisseur_id')->nullable()->constrained('fournisseurs')->onDelete('set null'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('products'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_04_115430_update_products_table_image_upload.php b/thanasoft-back/database/migrations/2025_11_04_115430_update_products_table_image_upload.php new file mode 100644 index 0000000..d5b4f13 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_04_115430_update_products_table_image_upload.php @@ -0,0 +1,36 @@ +dropColumn('photo_url'); + + // Add new image column for file uploads + $table->string('image')->nullable()->after('conditionnement_unite'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('products', function (Blueprint $table) { + // Remove the image column + $table->dropColumn('image'); + + // Restore the old photo_url column + $table->string('photo_url')->nullable(); + }); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_04_122449_create_product_categories_table.php b/thanasoft-back/database/migrations/2025_11_04_122449_create_product_categories_table.php new file mode 100644 index 0000000..ed280ab --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_04_122449_create_product_categories_table.php @@ -0,0 +1,41 @@ +id(); + $table->unsignedBigInteger('parent_id')->nullable(); + $table->string('code', 64); + $table->string('name', 191); + $table->text('description')->nullable(); + $table->boolean('active')->default(true); + $table->timestamps(); + + $table->foreign('parent_id') + ->references('id') + ->on('product_categories') + ->onDelete('set null'); + + $table->index('code'); + $table->index('active'); + $table->index('parent_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('product_categories'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_05_070647_change_categorie_to_categorie_id_in_products_table.php b/thanasoft-back/database/migrations/2025_11_05_070647_change_categorie_to_categorie_id_in_products_table.php new file mode 100644 index 0000000..944b2cd --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_05_070647_change_categorie_to_categorie_id_in_products_table.php @@ -0,0 +1,60 @@ +foreignId('categorie_id')->nullable()->after('reference') + ->constrained('product_categories')->onDelete('set null'); + }); + + // Migrate existing data: map categorie string values to ProductCategory IDs + $categories = ProductCategory::all()->keyBy('name'); + + Product::chunk(100, function ($products) use ($categories) { + foreach ($products as $product) { + if ($product->categorie && isset($categories[$product->categorie])) { + $product->categorie_id = $categories[$product->categorie]->id; + $product->save(); + } + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // First, we need to restore the categorie data from categorie_id + $categories = ProductCategory::all()->keyBy('id'); + + Product::chunk(100, function ($products) use ($categories) { + foreach ($products as $product) { + if ($product->categorie_id && isset($categories[$product->categorie_id])) { + $product->categorie = $categories[$product->categorie_id]->name; + $product->save(); + } + } + }); + + Schema::table('products', function (Blueprint $table) { + // Drop the foreign key constraint first + $table->dropForeign(['categorie_id']); + + // Drop the categorie_id column + $table->dropColumn('categorie_id'); + }); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_05_115756_create_employees_table.php b/thanasoft-back/database/migrations/2025_11_05_115756_create_employees_table.php new file mode 100644 index 0000000..121de6f --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_05_115756_create_employees_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('first_name', 191)->comment('Prénom de l\'employé'); + $table->string('last_name', 191)->comment('Nom de famille de l\'employé'); + $table->string('email', 191)->nullable()->comment('Adresse email de l\'employé'); + $table->string('phone', 50)->nullable()->comment('Numéro de téléphone de l\'employé'); + $table->string('job_title', 191)->nullable()->comment('Intitulé du poste'); + $table->date('hire_date')->nullable()->comment('Date d\'embauche'); + $table->boolean('active')->default(true)->comment('Statut actif de l\'employé'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('employees'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_05_115807_create_thanatopractitioners_table.php b/thanasoft-back/database/migrations/2025_11_05_115807_create_thanatopractitioners_table.php new file mode 100644 index 0000000..6b56747 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_05_115807_create_thanatopractitioners_table.php @@ -0,0 +1,39 @@ +id(); + $table->unsignedBigInteger('employee_id')->unique()->comment('ID de l\'employé associé'); + $table->string('diploma_number', 191)->nullable()->comment('Numéro de diplôme'); + $table->date('diploma_date')->nullable()->comment('Date d\'obtention du diplôme'); + $table->string('authorization_number', 191)->nullable()->comment('Numéro d\'autorisation'); + $table->date('authorization_issue_date')->nullable()->comment('Date de délivrance de l\'autorisation'); + $table->date('authorization_expiry_date')->nullable()->comment('Date d\'expiration de l\'autorisation'); + $table->text('notes')->nullable()->comment('Notes supplémentaires'); + $table->timestamps(); + + $table->foreign('employee_id') + ->references('id') + ->on('employees') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('thanatopractitioners'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_05_115820_create_practitioner_documents_table.php b/thanasoft-back/database/migrations/2025_11_05_115820_create_practitioner_documents_table.php new file mode 100644 index 0000000..85f7ff9 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_05_115820_create_practitioner_documents_table.php @@ -0,0 +1,44 @@ +id(); + $table->unsignedBigInteger('practitioner_id')->comment('ID du thanatopractitioner'); + $table->string('doc_type', 191)->comment('Type de document'); + $table->unsignedBigInteger('file_id')->nullable()->comment('ID du fichier associé'); + $table->date('issue_date')->nullable()->comment('Date de délivrance'); + $table->date('expiry_date')->nullable()->comment('Date d\'expiration'); + $table->string('status', 64)->nullable()->comment('Statut du document'); + $table->timestamps(); + + $table->foreign('practitioner_id') + ->references('id') + ->on('thanatopractitioners') + ->onDelete('cascade'); + + // Note: The files table might not exist yet, so we won't add this foreign key constraint + // $table->foreign('file_id') + // ->references('id') + // ->on('files') + // ->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('practitioner_documents'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_11_013751_create_table_files.php b/thanasoft-back/database/migrations/2025_11_11_013751_create_table_files.php new file mode 100644 index 0000000..d7ea2e6 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_11_013751_create_table_files.php @@ -0,0 +1,39 @@ +id(); // This creates an auto-incrementing BIGINT primary key + $table->string('file_name', 255); + $table->string('mime_type', 191)->nullable(); + $table->bigInteger('size_bytes')->nullable(); + $table->text('storage_uri'); + $table->char('sha256', 64)->nullable(); + $table->foreignId('uploaded_by')->nullable()->constrained('users')->onDelete('set null'); + $table->timestamp('uploaded_at')->useCurrent(); + $table->timestamps(); // Optional: adds created_at and updated_at columns + + // Add index for better performance on frequently searched columns + $table->index('sha256'); + $table->index('uploaded_by'); + $table->index('uploaded_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('files'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_11_100000_create_deceased_table.php b/thanasoft-back/database/migrations/2025_11_11_100000_create_deceased_table.php new file mode 100644 index 0000000..e076375 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_11_100000_create_deceased_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('last_name', 191)->nullable(false); + $table->string('first_name', 191)->nullable(); + $table->date('birth_date')->nullable(); + $table->date('death_date')->nullable(); + $table->string('place_of_death', 255)->nullable(); + $table->text('notes')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('deceased'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_11_100001_create_deceased_documents_table.php b/thanasoft-back/database/migrations/2025_11_11_100001_create_deceased_documents_table.php new file mode 100644 index 0000000..75f32c4 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_11_100001_create_deceased_documents_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('deceased_id')->constrained('deceased')->cascadeOnDelete(); + $table->string('doc_type', 64); + $table->foreignId('file_id')->nullable()->constrained('files')->nullOnDelete(); + $table->timestamp('generated_at')->useCurrent(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('deceased_documents'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_11_100002_create_interventions_table.php b/thanasoft-back/database/migrations/2025_11_11_100002_create_interventions_table.php new file mode 100644 index 0000000..8d468d1 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_11_100002_create_interventions_table.php @@ -0,0 +1,57 @@ +id(); + $table->foreignId('client_id')->constrained('clients'); + $table->foreignId('deceased_id')->nullable()->constrained('deceased')->nullOnDelete(); + $table->string('order_giver', 255)->nullable(); + $table->foreignId('location_id')->nullable()->constrained('client_locations')->nullOnDelete(); + $table->enum('type', [ + 'thanatopraxie', + 'toilette_mortuaire', + 'exhumation', + 'retrait_pacemaker', + 'retrait_bijoux', + 'autre' + ])->nullable(false); + $table->dateTime('scheduled_at')->nullable(); + $table->integer('duration_min')->nullable(); + $table->enum('status', [ + 'demande', + 'planifie', + 'en_cours', + 'termine', + 'annule' + ])->default('demande'); + $table->foreignId('assigned_practitioner_id')->nullable()->constrained('thanatopractitioners')->nullOnDelete(); + $table->integer('attachments_count')->default(0); + $table->text('notes')->nullable(); + $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + + // Indexes + $table->index(['client_id', 'status'], 'idx_interventions_company_status'); + $table->index('scheduled_at', 'idx_interventions_scheduled'); + $table->index(['client_id', 'scheduled_at'], 'idx_interventions_client_date'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('interventions'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_11_100003_create_intervention_attachments_table.php b/thanasoft-back/database/migrations/2025_11_11_100003_create_intervention_attachments_table.php new file mode 100644 index 0000000..7880e75 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_11_100003_create_intervention_attachments_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('intervention_id')->constrained('interventions')->cascadeOnDelete(); + $table->foreignId('file_id')->nullable()->constrained('files')->nullOnDelete(); + $table->string('label', 191)->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('intervention_attachments'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_11_100004_create_intervention_notifications_table.php b/thanasoft-back/database/migrations/2025_11_11_100004_create_intervention_notifications_table.php new file mode 100644 index 0000000..9bb620d --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_11_100004_create_intervention_notifications_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('intervention_id')->constrained('interventions')->cascadeOnDelete(); + $table->enum('channel', ['sms', 'mobile', 'email'])->nullable(false); + $table->string('destination', 191)->nullable(false); + $table->json('payload')->nullable(); + $table->string('status', 32)->default('queued'); + $table->timestamp('sent_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('intervention_notifications'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_19_131200_create_intervention_practitioner_table.php b/thanasoft-back/database/migrations/2025_11_19_131200_create_intervention_practitioner_table.php new file mode 100644 index 0000000..508a028 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_19_131200_create_intervention_practitioner_table.php @@ -0,0 +1,38 @@ +id(); + $table->foreignId('intervention_id')->constrained('interventions')->onDelete('cascade'); + $table->foreignId('practitioner_id')->constrained('thanatopractitioners')->onDelete('cascade'); + $table->enum('role', ['principal', 'assistant'])->default('principal'); + $table->timestamp('assigned_at')->useCurrent(); + $table->timestamps(); + + // Unique constraint to prevent duplicate assignments + $table->unique(['intervention_id', 'practitioner_id']); + + // Indexes for better query performance + $table->index('practitioner_id', 'idx_intervention_practitioner_practitioner'); + $table->index('intervention_id', 'idx_intervention_practitioner_intervention'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('intervention_practitioner'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_19_131500_remove_assigned_practitioner_id_from_interventions_table.php b/thanasoft-back/database/migrations/2025_11_19_131500_remove_assigned_practitioner_id_from_interventions_table.php new file mode 100644 index 0000000..9ef927b --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_19_131500_remove_assigned_practitioner_id_from_interventions_table.php @@ -0,0 +1,33 @@ +dropForeign(['assigned_practitioner_id']); + $table->dropColumn('assigned_practitioner_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // This migration is not easily reversible since we'd lose the many-to-many relationship + // In a real scenario, you'd want to handle this differently + Schema::table('interventions', function (Blueprint $table) { + $table->foreignId('assigned_practitioner_id')->nullable()->constrained('thanatopractitioners')->nullOnDelete(); + }); + } +}; diff --git a/thanasoft-back/database/migrations/2025_12_01_125100_add_polymorphic_to_intervention_attachments_table.php b/thanasoft-back/database/migrations/2025_12_01_125100_add_polymorphic_to_intervention_attachments_table.php new file mode 100644 index 0000000..f825d51 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_12_01_125100_add_polymorphic_to_intervention_attachments_table.php @@ -0,0 +1,41 @@ +string('attachable_type')->after('intervention_id')->nullable(); + $table->unsignedBigInteger('attachable_id')->after('attachable_type')->nullable(); + + // Add index for polymorphic queries + $table->index(['attachable_type', 'attachable_id'], 'polymorphic_attachment_index'); + + // Make intervention_id nullable for backward compatibility + // Existing records will still work with intervention_id + $table->unsignedBigInteger('intervention_id')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('intervention_attachments', function (Blueprint $table) { + $table->dropIndex('polymorphic_attachment_index'); + $table->dropColumn(['attachable_type', 'attachable_id']); + + // Restore intervention_id as required + $table->unsignedBigInteger('intervention_id')->nullable(false)->change(); + }); + } +}; diff --git a/thanasoft-back/database/migrations/2025_12_01_130000_create_file_attachments_table.php b/thanasoft-back/database/migrations/2025_12_01_130000_create_file_attachments_table.php new file mode 100644 index 0000000..0da66bf --- /dev/null +++ b/thanasoft-back/database/migrations/2025_12_01_130000_create_file_attachments_table.php @@ -0,0 +1,42 @@ +id(); + $table->foreignId('file_id')->constrained('files')->cascadeOnDelete(); + + // Polymorphic relationship columns + $table->string('attachable_type'); // intervention, client, deceased, etc. + $table->unsignedBigInteger('attachable_id'); // ID of the related model + + // Attachment metadata + $table->string('label')->nullable(); // Custom label for the attachment + $table->unsignedInteger('sort_order')->default(0); // For ordering attachments + + // Indexes for polymorphic queries + $table->index(['attachable_type', 'attachable_id'], 'polymorphic_attachment_index'); + $table->index(['file_id'], 'file_attachment_index'); + $table->index(['sort_order'], 'sort_order_index'); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('file_attachments'); + } +}; diff --git a/thanasoft-back/database/migrations/2026_01_05_155500_add_intervention_to_product_categories_table.php b/thanasoft-back/database/migrations/2026_01_05_155500_add_intervention_to_product_categories_table.php new file mode 100644 index 0000000..2406abf --- /dev/null +++ b/thanasoft-back/database/migrations/2026_01_05_155500_add_intervention_to_product_categories_table.php @@ -0,0 +1,32 @@ +boolean('intervention')->default(false)->after('description'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('product_categories', function (Blueprint $table) { + if (Schema::hasColumn('product_categories', 'intervention')) { + $table->dropColumn('intervention'); + } + }); + } +}; diff --git a/thanasoft-back/database/migrations/2026_01_05_164000_refactor_products_category_column.php b/thanasoft-back/database/migrations/2026_01_05_164000_refactor_products_category_column.php new file mode 100644 index 0000000..e23c1f6 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_01_05_164000_refactor_products_category_column.php @@ -0,0 +1,35 @@ +dropColumn('categorie'); + } + + // Ensure categorie_id exists and is FK (It should be from previous migration) + // If strictly needed we could check !Schema::hasColumn('products', 'categorie_id') but we know it failed on that. + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('products', function (Blueprint $table) { + if (!Schema::hasColumn('products', 'categorie')) { + $table->string('categorie')->nullable(); + } + }); + } +}; diff --git a/thanasoft-back/database/migrations/2026_01_05_165400_add_product_id_to_interventions_table.php b/thanasoft-back/database/migrations/2026_01_05_165400_add_product_id_to_interventions_table.php new file mode 100644 index 0000000..fbe27e0 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_01_05_165400_add_product_id_to_interventions_table.php @@ -0,0 +1,30 @@ +foreignId('product_id')->nullable()->after('location_id') + ->constrained('products')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('interventions', function (Blueprint $table) { + $table->dropForeign(['product_id']); + $table->dropColumn('product_id'); + }); + } +}; diff --git a/thanasoft-back/database/migrations/2026_01_06_105000_create_quotes_table.php b/thanasoft-back/database/migrations/2026_01_06_105000_create_quotes_table.php new file mode 100644 index 0000000..357da47 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_01_06_105000_create_quotes_table.php @@ -0,0 +1,40 @@ +id(); + $table->unsignedBigInteger('client_id'); + $table->unsignedBigInteger('group_id')->nullable(); + $table->string('reference', 191); + $table->enum('status', ['brouillon', 'envoye', 'accepte', 'refuse', 'expire', 'annule'])->default('brouillon')->index('idx_quotes_status'); + $table->date('quote_date')->default(now()); + $table->date('valid_until')->nullable(); + $table->char('currency', 3)->default('EUR'); + $table->decimal('total_ht', 14, 2)->default(0); + $table->decimal('total_tva', 14, 2)->default(0); + $table->decimal('total_ttc', 14, 2)->default(0); + $table->timestamps(); + + $table->foreign('client_id', 'fk_quotes_client')->references('id')->on('clients'); + $table->foreign('group_id', 'fk_quotes_group')->references('id')->on('client_groups')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('quotes'); + } +}; diff --git a/thanasoft-back/database/migrations/2026_01_06_124703_create_quote_lines_table.php b/thanasoft-back/database/migrations/2026_01_06_124703_create_quote_lines_table.php new file mode 100644 index 0000000..7ffe22a --- /dev/null +++ b/thanasoft-back/database/migrations/2026_01_06_124703_create_quote_lines_table.php @@ -0,0 +1,44 @@ +id(); + $table->foreignId('quote_id')->constrained('quotes')->onDelete('cascade'); + $table->foreignId('product_id')->nullable()->constrained('products')->onDelete('set null'); + $table->decimal('packages_qty', 14, 3)->nullable(); + $table->decimal('units_qty', 14, 3)->nullable(); + $table->text('description'); + $table->decimal('qty_base', 14, 3)->nullable(); + $table->decimal('unit_price', 12, 2); + $table->decimal('unit_price_per_package', 12, 2)->nullable(); + + // Tables do not exist yet, removing constraints + $table->foreignId('packaging_id')->nullable(); + $table->foreignId('tva_rate_id')->nullable(); + + $table->decimal('discount_pct', 5, 2)->default(0); + $table->decimal('total_ht', 14, 2); + + // If timestamps are needed (default model usually has them) + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('quote_lines'); + } +}; diff --git a/thanasoft-back/database/migrations/2026_01_07_081428_create_document_status_history_table.php b/thanasoft-back/database/migrations/2026_01_07_081428_create_document_status_history_table.php new file mode 100644 index 0000000..c73f7e2 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_01_07_081428_create_document_status_history_table.php @@ -0,0 +1,36 @@ +id(); + $table->enum('document_type', ['quote', 'invoice']); + $table->unsignedBigInteger('document_id'); + $table->string('old_status', 32)->nullable(); + $table->string('new_status', 32); + $table->unsignedBigInteger('changed_by')->nullable(); + $table->timestamp('changed_at')->useCurrent(); + $table->text('comment')->nullable(); + + $table->index(['document_type', 'document_id'], 'idx_dsh_doc'); + // Assuming we might want a foreign key for changed_by if users table exists, but user didn't explicitly ask for constraint, just column. I will leave it as column. + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('document_status_history'); + } +}; diff --git a/thanasoft-back/database/migrations/2026_01_08_115945_add_parent_fields_to_clients_table.php b/thanasoft-back/database/migrations/2026_01_08_115945_add_parent_fields_to_clients_table.php new file mode 100644 index 0000000..bc20a3a --- /dev/null +++ b/thanasoft-back/database/migrations/2026_01_08_115945_add_parent_fields_to_clients_table.php @@ -0,0 +1,30 @@ +boolean('is_parent')->nullable()->default(false); + $table->foreignId('parent_id')->nullable()->constrained('clients')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('clients', function (Blueprint $table) { + $table->dropForeign(['parent_id']); + $table->dropColumn(['is_parent', 'parent_id']); + }); + } +}; diff --git a/thanasoft-back/database/migrations/2026_01_09_000000_create_invoices_tables.php b/thanasoft-back/database/migrations/2026_01_09_000000_create_invoices_tables.php new file mode 100644 index 0000000..679e6fb --- /dev/null +++ b/thanasoft-back/database/migrations/2026_01_09_000000_create_invoices_tables.php @@ -0,0 +1,67 @@ +id(); + $table->unsignedBigInteger('client_id'); + $table->unsignedBigInteger('group_id')->nullable(); + $table->unsignedBigInteger('source_quote_id')->nullable(); + $table->string('invoice_number', 191); + $table->enum('status', ['brouillon', 'emise', 'envoyee', 'partiellement_payee', 'payee', 'echue', 'annulee', 'avoir'])->default('brouillon'); + $table->date('invoice_date')->useCurrent(); + $table->date('due_date')->nullable(); + $table->char('currency', 3)->default('EUR'); + $table->decimal('total_ht', 14, 2)->default(0); + $table->decimal('total_tva', 14, 2)->default(0); + $table->decimal('total_ttc', 14, 2)->default(0); + $table->enum('e_invoice_status', ['cree', 'transmis', 'accepte', 'refuse', 'en_litige', 'acquitte', 'archive'])->nullable()->default('cree'); + $table->timestamps(); + + $table->index(['status', 'due_date'], 'idx_invoices_status_due'); + + $table->foreign('client_id', 'fk_invoices_client')->references('id')->on('clients'); + $table->foreign('group_id', 'fk_invoices_group')->references('id')->on('client_groups')->onDelete('set null'); + $table->foreign('source_quote_id', 'fk_invoices_quote')->references('id')->on('quotes')->onDelete('set null'); + + }); + + Schema::create('invoice_lines', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('invoice_id'); + $table->unsignedBigInteger('product_id')->nullable(); + $table->unsignedBigInteger('packaging_id')->nullable(); + $table->decimal('packages_qty', 14, 3)->nullable(); + $table->decimal('units_qty', 14, 3)->nullable(); + $table->text('description'); + $table->decimal('qty_base', 14, 3)->nullable(); + $table->decimal('unit_price', 12, 2); + $table->decimal('unit_price_per_package', 12, 2)->nullable(); + $table->unsignedBigInteger('tva_rate_id')->nullable(); + $table->decimal('discount_pct', 5, 2)->default(0); + $table->decimal('total_ht', 14, 2); + + $table->foreign('invoice_id', 'fk_il_invoice')->references('id')->on('invoices')->onDelete('cascade'); + $table->foreign('product_id', 'fk_il_product')->references('id')->on('products')->onDelete('set null'); + // Note: packaging_id and tva_rate_id FK constraints removed - referenced tables don't exist + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('invoice_lines'); + Schema::dropIfExists('invoices'); + } +}; diff --git a/thanasoft-back/database/migrations/2026_01_12_124826_create_client_activity_timelines_table.php b/thanasoft-back/database/migrations/2026_01_12_124826_create_client_activity_timelines_table.php new file mode 100644 index 0000000..c4d4f6e --- /dev/null +++ b/thanasoft-back/database/migrations/2026_01_12_124826_create_client_activity_timelines_table.php @@ -0,0 +1,78 @@ +id(); + $table->unsignedBigInteger('client_id'); + $table->enum('actor_type', ['user', 'system', 'api'])->default('user'); + $table->unsignedBigInteger('actor_user_id')->nullable(); + + $table->enum('event_type', [ + 'quote_created', + 'quote_sent', + 'quote_status_changed', + 'invoice_created', + 'invoice_sent', + 'invoice_paid', + 'intervention_created', + 'intervention_scheduled', + 'intervention_completed', + 'call', + 'email_sent', + 'email_received', + 'attachment_sent', + 'attachment_received', + 'client_created', + 'client_info_updated', + 'note' + ]); + + $table->enum('entity_type', [ + 'quote', + 'invoice', + 'intervention', + 'email', + 'file', + 'client', + 'payment' + ])->nullable(); + + $table->unsignedBigInteger('entity_id')->nullable(); + $table->string('title'); + $table->text('description')->nullable(); + $table->json('metadata')->nullable(); + $table->dateTime('created_at')->useCurrent(); + + // Indexes + $table->index(['entity_type', 'entity_id'], 'idx_cat_entity'); + $table->index(['event_type'], 'idx_cat_event'); + + // Foreign Keys + $table->foreign('client_id', 'fk_cat_client') + ->references('id')->on('clients') + ->onDelete('cascade'); + + $table->foreign('actor_user_id', 'fk_cat_user') + ->references('id')->on('users') + ->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('client_activity_timeline'); + } +}; diff --git a/thanasoft-back/database/migrations/2026_01_13_083624_add_avatar_to_clients_table.php b/thanasoft-back/database/migrations/2026_01_13_083624_add_avatar_to_clients_table.php new file mode 100644 index 0000000..319a589 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_01_13_083624_add_avatar_to_clients_table.php @@ -0,0 +1,28 @@ +string('avatar')->nullable()->after('phone'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('clients', function (Blueprint $table) { + $table->dropColumn('avatar'); + }); + } +}; diff --git a/thanasoft-back/database/migrations/2026_01_26_115000_create_avoirs_tables.php b/thanasoft-back/database/migrations/2026_01_26_115000_create_avoirs_tables.php new file mode 100644 index 0000000..d2e384f --- /dev/null +++ b/thanasoft-back/database/migrations/2026_01_26_115000_create_avoirs_tables.php @@ -0,0 +1,112 @@ +id(); + $table->unsignedBigInteger('client_id'); + $table->unsignedBigInteger('invoice_id')->nullable(); // The original invoice being credited + $table->unsignedBigInteger('group_id')->nullable(); + + $table->string('avoir_number', 191); + $table->enum('status', ['brouillon', 'emis', 'applique', 'annule'])->default('brouillon'); + + // Dates + $table->date('avoir_date')->useCurrent(); + $table->date('due_date')->nullable(); + + // Currency and amounts (typically negative or representing credit) + $table->char('currency', 3)->default('EUR'); + $table->decimal('total_ht', 14, 2)->default(0); + $table->decimal('total_tva', 14, 2)->default(0); + $table->decimal('total_ttc', 14, 2)->default(0); + + // Reason for credit note + $table->enum('reason_type', [ + 'remboursement_total', + 'remboursement_partiel', + 'reduction', + 'erreur_facturation', + 'retour_marchandise', + 'accord_commercial', + 'autre' + ])->default('autre'); + $table->text('reason_description')->nullable(); + + // E-invoicing status (if applicable) + $table->enum('e_invoice_status', [ + 'cree', 'transmis', 'accepte', 'refuse', 'en_litige', 'acquitte', 'archive' + ])->nullable()->default('cree'); + + // Refund information (for accounting reconciliation) + $table->enum('refund_status', [ + 'non_rembourse', 'en_cours', 'partiellement_rembourse', 'rembourse', 'compense' + ])->default('non_rembourse'); + $table->date('refund_date')->nullable(); + $table->enum('refund_method', [ + 'virement', 'cheque', 'carte_credit', 'compensation_future', 'autre' + ])->nullable(); + + // Compensation (if credit is applied to future invoices) + $table->unsignedBigInteger('compensation_invoice_id')->nullable(); + $table->decimal('compensation_amount', 14, 2)->default(0); + + $table->timestamps(); + + // Indexes for performance + $table->index(['status', 'due_date'], 'idx_avoirs_status_due'); + $table->index('invoice_id', 'idx_avoirs_invoice'); + $table->index('avoir_number', 'idx_avoirs_number'); + $table->index('refund_status', 'idx_avoirs_refund_status'); + $table->index('avoir_date', 'idx_avoirs_date'); + + // Foreign key constraints + $table->foreign('client_id', 'fk_avoirs_client')->references('id')->on('clients')->onDelete('cascade'); + $table->foreign('invoice_id', 'fk_avoirs_invoice')->references('id')->on('invoices')->onDelete('restrict'); + $table->foreign('group_id', 'fk_avoirs_group')->references('id')->on('client_groups')->onDelete('set null'); + $table->foreign('compensation_invoice_id', 'fk_avoirs_compensation')->references('id')->on('invoices')->onDelete('set null'); + }); + + Schema::create('avoir_lines', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('avoir_id'); + $table->unsignedBigInteger('product_id')->nullable(); + $table->unsignedBigInteger('invoice_line_id')->nullable(); // Reference to the original invoice line if applicable + + $table->string('description', 500); + $table->decimal('quantity', 10, 3)->default(1); + $table->decimal('unit_price', 14, 4); + $table->decimal('tva_rate', 5, 2)->default(0); + $table->decimal('total_ht', 14, 2); + $table->decimal('total_tva', 14, 2)->default(0); + $table->decimal('total_ttc', 14, 2)->default(0); + $table->text('notes')->nullable(); + + $table->timestamps(); + + $table->index('avoir_id', 'idx_avoir_lines_avoir'); + + $table->foreign('avoir_id', 'fk_avoir_lines_avoir')->references('id')->on('avoirs')->onDelete('cascade'); + $table->foreign('product_id', 'fk_avoir_lines_product')->references('id')->on('products')->onDelete('set null'); + $table->foreign('invoice_line_id', 'fk_avoir_lines_invoice_line')->references('id')->on('invoice_lines')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('avoir_lines'); + Schema::dropIfExists('avoirs'); + } +}; diff --git a/thanasoft-back/database/migrations/2026_01_26_120000_create_supplier_system_tables.php b/thanasoft-back/database/migrations/2026_01_26_120000_create_supplier_system_tables.php new file mode 100644 index 0000000..9accb28 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_01_26_120000_create_supplier_system_tables.php @@ -0,0 +1,117 @@ +id(); + $table->unsignedBigInteger('fournisseur_id'); // Use existing fournisseurs table + + $table->string('po_number', 191); + $table->enum('status', ['brouillon', 'confirmee', 'livree', 'facturee', 'annulee'])->default('brouillon'); + $table->date('order_date')->useCurrent(); + $table->date('expected_date')->nullable(); + $table->char('currency', 3)->default('EUR'); + $table->decimal('total_ht', 14, 2)->default(0); + $table->decimal('total_tva', 14, 2)->default(0); + $table->decimal('total_ttc', 14, 2)->default(0); + $table->text('notes')->nullable(); + $table->string('delivery_address')->nullable(); + + $table->timestamps(); + + $table->unique('po_number', 'uq_po_number'); + $table->index('status', 'idx_po_status'); + $table->index('order_date', 'idx_po_order_date'); + + $table->foreign('fournisseur_id', 'fk_po_fournisseur')->references('id')->on('fournisseurs')->onDelete('cascade'); + }); + + // Purchase Order Lines + Schema::create('purchase_order_lines', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('purchase_order_id'); + $table->unsignedBigInteger('product_id')->nullable(); + + $table->text('description'); + $table->decimal('quantity', 14, 3)->default(1); + $table->decimal('unit_price', 12, 2); + $table->decimal('tva_rate', 5, 2)->default(0); // Use rate directly, no FK to tva_rates + $table->decimal('discount_pct', 5, 2)->default(0); + $table->decimal('total_ht', 14, 2); + + $table->timestamps(); + + $table->foreign('purchase_order_id', 'fk_pol_po')->references('id')->on('purchase_orders')->onDelete('cascade'); + $table->foreign('product_id', 'fk_pol_product')->references('id')->on('products')->onDelete('set null'); + }); + + + + // Supplier Invoices (Factures Fournisseurs) + Schema::create('supplier_invoices', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('fournisseur_id'); + $table->unsignedBigInteger('purchase_order_id')->nullable(); // Optional link to purchase order + + $table->string('invoice_number', 191); + $table->date('invoice_date')->useCurrent(); + $table->date('due_date')->nullable(); + $table->enum('status', ['brouillon', 'en_attente', 'payee', 'annulee'])->default('brouillon'); + $table->char('currency', 3)->default('EUR'); + $table->decimal('total_ht', 14, 2)->default(0); + $table->decimal('total_tva', 14, 2)->default(0); + $table->decimal('total_ttc', 14, 2)->default(0); + $table->text('notes')->nullable(); + + $table->timestamps(); + + $table->unique(['fournisseur_id', 'invoice_number'], 'uq_supplier_invoice'); + $table->index('status', 'idx_si_status'); + $table->index('invoice_date', 'idx_si_invoice_date'); + + $table->foreign('fournisseur_id', 'fk_si_fournisseur')->references('id')->on('fournisseurs'); + $table->foreign('purchase_order_id', 'fk_si_po')->references('id')->on('purchase_orders')->onDelete('set null'); + }); + + // Supplier Invoice Lines + Schema::create('supplier_invoice_lines', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('supplier_invoice_id'); + $table->unsignedBigInteger('product_id')->nullable(); + $table->unsignedBigInteger('purchase_order_line_id')->nullable(); // Link to original order line + + $table->text('description'); + $table->decimal('quantity', 14, 3)->default(1); + $table->decimal('unit_price', 12, 2); + $table->decimal('tva_rate', 5, 2)->default(0); + $table->decimal('total_ht', 14, 2); + + $table->timestamps(); + + $table->foreign('supplier_invoice_id', 'fk_sil_si')->references('id')->on('supplier_invoices')->onDelete('cascade'); + $table->foreign('product_id', 'fk_sil_product')->references('id')->on('products')->onDelete('set null'); + $table->foreign('purchase_order_line_id', 'fk_sil_pol')->references('id')->on('purchase_order_lines')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('supplier_invoice_lines'); + Schema::dropIfExists('supplier_invoices'); + + Schema::dropIfExists('purchase_order_lines'); + Schema::dropIfExists('purchase_orders'); + } +}; diff --git a/thanasoft-back/database/migrations/2026_02_02_153000_create_warehouses_table.php b/thanasoft-back/database/migrations/2026_02_02_153000_create_warehouses_table.php new file mode 100644 index 0000000..015a336 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_02_02_153000_create_warehouses_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('name', 191); + $table->string('address_line1', 255)->nullable(); + $table->string('address_line2', 255)->nullable(); + $table->string('postal_code', 20)->nullable(); + $table->string('city', 191)->nullable(); + $table->char('country_code', 2)->default('FR'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('warehouses'); + } +}; diff --git a/thanasoft-back/database/migrations/2026_02_02_153001_create_product_packagings_table.php b/thanasoft-back/database/migrations/2026_02_02_153001_create_product_packagings_table.php new file mode 100644 index 0000000..7230b80 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_02_02_153001_create_product_packagings_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('product_id')->constrained()->onDelete('cascade'); + $table->string('name', 191); + $table->decimal('qty_base', 14, 3); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('product_packagings'); + } +}; diff --git a/thanasoft-back/database/migrations/2026_02_02_153002_create_stock_items_table.php b/thanasoft-back/database/migrations/2026_02_02_153002_create_stock_items_table.php new file mode 100644 index 0000000..bcfbe73 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_02_02_153002_create_stock_items_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('product_id')->constrained()->onDelete('cascade'); + $table->foreignId('warehouse_id')->constrained()->onDelete('cascade'); + $table->decimal('qty_on_hand_base', 14, 3)->default(0); + $table->decimal('safety_stock_base', 14, 3)->default(0); + $table->timestamps(); + + $table->unique(['product_id', 'warehouse_id'], 'uq_stock_item'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('stock_items'); + } +}; diff --git a/thanasoft-back/database/migrations/2026_02_02_153003_create_stock_moves_table.php b/thanasoft-back/database/migrations/2026_02_02_153003_create_stock_moves_table.php new file mode 100644 index 0000000..3a1b0e3 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_02_02_153003_create_stock_moves_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('product_id')->constrained()->onDelete('cascade'); + $table->foreignId('from_warehouse_id')->nullable()->constrained('warehouses')->onDelete('set null'); + $table->foreignId('to_warehouse_id')->nullable()->constrained('warehouses')->onDelete('set null'); + $table->foreignId('packaging_id')->nullable()->constrained('product_packagings')->onDelete('set null'); + $table->decimal('packages_qty', 14, 3)->nullable(); + $table->decimal('units_qty', 14, 3)->nullable(); + $table->decimal('qty_base', 14, 3); + $table->string('move_type', 64); + $table->string('ref_type', 64)->nullable(); + $table->unsignedBigInteger('ref_id')->nullable(); + $table->timestamp('moved_at')->useCurrent(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('stock_moves'); + } +}; diff --git a/thanasoft-back/database/migrations/2026_02_03_123000_create_goods_receipt_tables.php b/thanasoft-back/database/migrations/2026_02_03_123000_create_goods_receipt_tables.php new file mode 100644 index 0000000..93cbf24 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_02_03_123000_create_goods_receipt_tables.php @@ -0,0 +1,82 @@ +id(); + $table->string('name', 50); + $table->decimal('rate', 5, 2); + $table->timestamps(); + }); + } + + // Goods Receipts (Réceptions de marchandises) + Schema::create('goods_receipts', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('purchase_order_id'); + $table->unsignedBigInteger('warehouse_id'); // entrepôt de réception + + $table->string('receipt_number', 191); + $table->date('receipt_date')->useCurrent(); // Default handled by DB if possible or model + $table->enum('status', ['draft', 'posted'])->default('draft'); + $table->timestamp('created_at')->useCurrent(); + // Note: Laravel default timestamps are created_at and updated_at. + // The user asked for created_at DEFAULT CURRENT_TIMESTAMP. + // Laravel's $table->timestamps() creates both created_at and updated_at nullable. + // We will stick to standard Laravel behavior but can add default if strictly needed. + // For now, let's use standard timestamps + specific columns requested. + $table->timestamp('updated_at')->nullable(); + + $table->unique(['purchase_order_id', 'receipt_number'], 'uq_gr_po_number'); + + $table->foreign('purchase_order_id', 'fk_gr_po')->references('id')->on('purchase_orders')->onDelete('cascade'); + $table->foreign('warehouse_id', 'fk_gr_wh')->references('id')->on('warehouses'); + }); + + // Goods Receipt Lines + Schema::create('goods_receipt_lines', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('goods_receipt_id'); + $table->unsignedBigInteger('product_id'); + $table->unsignedBigInteger('packaging_id')->nullable(); + + $table->decimal('packages_qty_received', 14, 3)->nullable(); // nb de colis reçus + $table->decimal('units_qty_received', 14, 3)->nullable(); // nb d’unités reçues (si pas de colis) + $table->decimal('qty_received_base', 14, 3)->nullable(); // quantité base reçue (calculable) + $table->decimal('unit_price', 12, 2)->nullable(); // par unité produit + $table->decimal('unit_price_per_package', 12, 2)->nullable();// par colis + $table->unsignedBigInteger('tva_rate_id')->nullable(); + + $table->timestamps(); + + $table->foreign('goods_receipt_id', 'fk_grl_gr')->references('id')->on('goods_receipts')->onDelete('cascade'); + $table->foreign('product_id', 'fk_grl_product')->references('id')->on('products'); + $table->foreign('packaging_id', 'fk_grl_packaging')->references('id')->on('product_packagings')->onDelete('set null'); + $table->foreign('tva_rate_id', 'fk_grl_tva')->references('id')->on('tva_rates')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('goods_receipt_lines'); + Schema::dropIfExists('goods_receipts'); + // We do not drop tva_rates here blindly as it might have been created elsewhere if we decide to separate later. + // But since we created it conditionally, we can leave it or drop it if we are sure we own it. + // Given the instructions, I'll drop it if it's safe, but usually safe to leave shared tables or manage in separate migration. + // For this task, I'll only drop what I definitely created exclusively. + Schema::dropIfExists('tva_rates'); + } +}; diff --git a/thanasoft-back/database/migrations/2026_03_04_140000_create_price_lists_table.php b/thanasoft-back/database/migrations/2026_03_04_140000_create_price_lists_table.php new file mode 100644 index 0000000..d8c5065 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_03_04_140000_create_price_lists_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('name', 191); + $table->date('valid_from')->nullable(); + $table->date('valid_to')->nullable(); + $table->boolean('is_default')->default(false); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('price_lists'); + } +}; + diff --git a/thanasoft-back/database/migrations/2026_03_04_140100_add_price_list_id_to_client_groups_table.php b/thanasoft-back/database/migrations/2026_03_04_140100_add_price_list_id_to_client_groups_table.php new file mode 100644 index 0000000..09fdd35 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_03_04_140100_add_price_list_id_to_client_groups_table.php @@ -0,0 +1,33 @@ +foreignId('price_list_id') + ->nullable() + ->after('description') + ->constrained('price_lists') + ->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('client_groups', function (Blueprint $table) { + $table->dropForeign(['price_list_id']); + $table->dropColumn('price_list_id'); + }); + } +}; + diff --git a/thanasoft-back/database/migrations/2026_03_04_140200_create_product_price_list_table.php b/thanasoft-back/database/migrations/2026_03_04_140200_create_product_price_list_table.php new file mode 100644 index 0000000..859cc1c --- /dev/null +++ b/thanasoft-back/database/migrations/2026_03_04_140200_create_product_price_list_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('product_id')->constrained('products')->cascadeOnDelete(); + $table->foreignId('price_list_id')->constrained('price_lists')->cascadeOnDelete(); + $table->decimal('price', 10, 2); + $table->timestamps(); + + $table->unique(['product_id', 'price_list_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('product_price_list'); + } +}; + diff --git a/thanasoft-back/database/migrations/2026_03_17_124022_add_invoice_id_intervention.php b/thanasoft-back/database/migrations/2026_03_17_124022_add_invoice_id_intervention.php new file mode 100644 index 0000000..c0a8220 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_03_17_124022_add_invoice_id_intervention.php @@ -0,0 +1,31 @@ +foreignId('invoice_id')->nullable()->constrained('invoices')->nullOnDelete(); + $table->foreignId('quote_id')->nullable()->constrained('quotes')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('interventions', function (Blueprint $table) { + $table->dropForeign(['invoice_id']); + $table->dropColumn('invoice_id'); + $table->dropForeign(['quote_id']); + $table->dropColumn('quote_id'); + }); + } +}; \ No newline at end of file diff --git a/thanasoft-back/database/migrations/2026_04_02_082000_make_quote_client_nullable.php b/thanasoft-back/database/migrations/2026_04_02_082000_make_quote_client_nullable.php new file mode 100644 index 0000000..184a7f6 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_04_02_082000_make_quote_client_nullable.php @@ -0,0 +1,28 @@ +unsignedBigInteger('client_id')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('quotes', function (Blueprint $table) { + $table->unsignedBigInteger('client_id')->nullable(false)->change(); + }); + } +}; diff --git a/thanasoft-back/database/migrations/2026_04_02_151000_make_invoice_client_nullable.php b/thanasoft-back/database/migrations/2026_04_02_151000_make_invoice_client_nullable.php new file mode 100644 index 0000000..8e25eb4 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_04_02_151000_make_invoice_client_nullable.php @@ -0,0 +1,28 @@ +unsignedBigInteger('client_id')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('invoices', function (Blueprint $table) { + $table->unsignedBigInteger('client_id')->nullable(false)->change(); + }); + } +}; diff --git a/thanasoft-back/database/migrations/2026_04_08_111700_add_user_id_to_employees_table.php b/thanasoft-back/database/migrations/2026_04_08_111700_add_user_id_to_employees_table.php new file mode 100644 index 0000000..1b0f8d9 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_04_08_111700_add_user_id_to_employees_table.php @@ -0,0 +1,33 @@ +foreignId('user_id') + ->nullable() + ->after('id') + ->constrained('users') + ->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('employees', function (Blueprint $table) { + $table->dropForeign(['user_id']); + $table->dropColumn('user_id'); + }); + } +}; diff --git a/thanasoft-back/database/migrations/2026_04_08_113400_make_users_password_nullable.php b/thanasoft-back/database/migrations/2026_04_08_113400_make_users_password_nullable.php new file mode 100644 index 0000000..3bd91b0 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_04_08_113400_make_users_password_nullable.php @@ -0,0 +1,28 @@ +string('password')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->string('password')->nullable(false)->change(); + }); + } +}; diff --git a/thanasoft-back/database/migrations/2026_04_15_111321_create_convois_and_vehicules_tables.php b/thanasoft-back/database/migrations/2026_04_15_111321_create_convois_and_vehicules_tables.php new file mode 100644 index 0000000..717c421 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_04_15_111321_create_convois_and_vehicules_tables.php @@ -0,0 +1,77 @@ +id(); + $table->string('photo_file_name')->nullable(); + $table->string('photo_file_url')->nullable(); + $table->string('photo_mime_type')->nullable(); + $table->unsignedBigInteger('photo_size')->nullable(); + $table->string('brand'); + $table->string('model'); + $table->string('registration_number')->unique(); + $table->enum('vehicle_type', ['hearse', 'transport_vehicle', 'utility', 'sedan'])->default('utility'); + $table->enum('fuel_type', ['diesel', 'petrol', 'electric', 'hybrid'])->default('diesel'); + $table->unsignedSmallInteger('year')->nullable(); + $table->foreignId('primary_user_id')->nullable()->constrained('employees')->nullOnDelete(); + $table->enum('status', ['active', 'maintenance', 'out_of_service'])->default('active'); + $table->text('notes')->nullable(); + $table->timestamps(); + + $table->index(['brand']); + $table->index(['status']); + $table->index(['primary_user_id']); + }); + + Schema::create('convoys', function (Blueprint $table) { + $table->id(); + $table->foreignId('deceased_id')->constrained('deceased')->cascadeOnDelete(); + $table->foreignId('client_id')->nullable()->constrained('clients')->nullOnDelete(); + $table->foreignId('vehicle_id')->nullable()->constrained('vehicles')->nullOnDelete(); + $table->string('mission_title')->nullable(); + $table->enum('convoy_type', ['local', 'national', 'international'])->default('local'); + $table->enum('transport_mode', ['road', 'air', 'sea', 'rail'])->default('road'); + $table->enum('status', ['planned', 'in_progress', 'completed', 'cancelled'])->default('planned'); + $table->dateTime('planned_start_at'); + $table->dateTime('estimated_end_at')->nullable(); + $table->string('family_email')->nullable(); + $table->boolean('automatic_notifications')->default(true); + $table->enum('departure_location_selection_mode', ['place', 'manual'])->default('place'); + $table->unsignedBigInteger('departure_location_id')->nullable(); + $table->string('departure_name')->nullable(); + $table->string('departure_address')->nullable(); + $table->string('departure_city')->nullable(); + $table->string('departure_postal_code', 20)->nullable(); + $table->string('departure_country_code', 2)->nullable(); + $table->decimal('departure_latitude', 10, 7)->nullable(); + $table->decimal('departure_longitude', 10, 7)->nullable(); + $table->text('departure_additional_details')->nullable(); + $table->timestamps(); + + $table->index(['deceased_id']); + $table->index(['client_id']); + $table->index(['vehicle_id']); + $table->index(['status']); + $table->index(['planned_start_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('convoys'); + Schema::dropIfExists('vehicles'); + } +}; diff --git a/thanasoft-back/database/migrations/2026_04_27_144242_create_permission_tables.php b/thanasoft-back/database/migrations/2026_04_27_144242_create_permission_tables.php new file mode 100644 index 0000000..66ce1f9 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_04_27_144242_create_permission_tables.php @@ -0,0 +1,134 @@ +engine('InnoDB'); + $table->bigIncrements('id'); // permission id + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); + $table->timestamps(); + + $table->unique(['name', 'guard_name']); + }); + + Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) { + // $table->engine('InnoDB'); + $table->bigIncrements('id'); // role id + if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing + $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); + $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); + } + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); + $table->timestamps(); + if ($teams || config('permission.testing')) { + $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']); + } else { + $table->unique(['name', 'guard_name']); + } + }); + + Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { + $table->unsignedBigInteger($pivotPermission); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index'); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } else { + $table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } + + }); + + Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { + $table->unsignedBigInteger($pivotRole); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } else { + $table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } + }); + + Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) { + $table->unsignedBigInteger($pivotPermission); + $table->unsignedBigInteger($pivotRole); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->onDelete('cascade'); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->onDelete('cascade'); + + $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary'); + }); + + app('cache') + ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) + ->forget(config('permission.cache.key')); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $tableNames = config('permission.table_names'); + + throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.'); + + Schema::drop($tableNames['role_has_permissions']); + Schema::drop($tableNames['model_has_roles']); + Schema::drop($tableNames['model_has_permissions']); + Schema::drop($tableNames['roles']); + Schema::drop($tableNames['permissions']); + } +}; diff --git a/thanasoft-back/database/migrations/2026_05_04_120000_create_webmail_messages_table.php b/thanasoft-back/database/migrations/2026_05_04_120000_create_webmail_messages_table.php new file mode 100644 index 0000000..0ee2bd0 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_05_04_120000_create_webmail_messages_table.php @@ -0,0 +1,40 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('message_uid')->nullable()->index(); + $table->string('direction', 20); + $table->string('folder', 30)->default('inbox')->index(); + $table->string('status', 30)->default('received')->index(); + $table->string('from_email')->nullable(); + $table->string('from_name')->nullable(); + $table->json('to_recipients'); + $table->json('cc_recipients')->nullable(); + $table->json('bcc_recipients')->nullable(); + $table->string('subject')->nullable(); + $table->longText('body')->nullable(); + $table->text('snippet')->nullable(); + $table->json('attachments')->nullable(); + $table->json('metadata')->nullable(); + $table->timestamp('read_at')->nullable()->index(); + $table->timestamp('starred_at')->nullable()->index(); + $table->timestamp('sent_at')->nullable()->index(); + $table->timestamp('received_at')->nullable()->index(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('webmail_messages'); + } +}; \ No newline at end of file diff --git a/thanasoft-back/database/migrations/2026_05_04_170000_create_user_mailbox_settings_table.php b/thanasoft-back/database/migrations/2026_05_04_170000_create_user_mailbox_settings_table.php new file mode 100644 index 0000000..892d547 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_05_04_170000_create_user_mailbox_settings_table.php @@ -0,0 +1,39 @@ +id(); + $table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete(); + $table->string('imap_host')->nullable(); + $table->unsignedSmallInteger('imap_port')->nullable(); + $table->string('imap_encryption', 10)->nullable(); + $table->boolean('imap_validate_cert')->default(true); + $table->string('imap_username')->nullable(); + $table->text('imap_password')->nullable(); + $table->string('imap_folder')->default('INBOX'); + $table->string('smtp_host')->nullable(); + $table->unsignedSmallInteger('smtp_port')->nullable(); + $table->string('smtp_encryption', 10)->nullable(); + $table->boolean('smtp_validate_cert')->default(true); + $table->string('smtp_username')->nullable(); + $table->text('smtp_password')->nullable(); + $table->string('smtp_from_address')->nullable(); + $table->string('smtp_from_name')->nullable(); + $table->timestamp('last_synced_at')->nullable(); + $table->text('last_sync_error')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('user_mailbox_settings'); + } +}; \ No newline at end of file diff --git a/thanasoft-back/database/seeders/AdminAccessSeeder.php b/thanasoft-back/database/seeders/AdminAccessSeeder.php new file mode 100644 index 0000000..603cea3 --- /dev/null +++ b/thanasoft-back/database/seeders/AdminAccessSeeder.php @@ -0,0 +1,39 @@ +firstOrCreate([ + 'name' => 'config.view_roles', + 'guard_name' => 'sanctum', + ]); + + $role = Role::query()->firstOrCreate([ + 'name' => 'administrator', + 'guard_name' => 'sanctum', + ]); + + $role->givePermissionTo($permission); + + $adminUser = User::query()->updateOrCreate( + ['email' => 'admin@admin.com'], + [ + 'name' => 'Admin User', + 'password' => 'password', + ] + ); + + $adminUser->assignRole($role); + } +} \ No newline at end of file diff --git a/thanasoft-back/database/seeders/ClientGroupSeeder.php b/thanasoft-back/database/seeders/ClientGroupSeeder.php new file mode 100644 index 0000000..db17be4 --- /dev/null +++ b/thanasoft-back/database/seeders/ClientGroupSeeder.php @@ -0,0 +1,34 @@ + $group, + 'description' => $faker->sentence, + 'price_list_id' => null, // volontairement null + ]); + } + } +} \ No newline at end of file diff --git a/thanasoft-back/database/seeders/ClientSeeder.php b/thanasoft-back/database/seeders/ClientSeeder.php new file mode 100644 index 0000000..b1fb1a6 --- /dev/null +++ b/thanasoft-back/database/seeders/ClientSeeder.php @@ -0,0 +1,48 @@ +toArray(); + + for ($i = 0; $i < 20; $i++) { + Client::create([ + 'name' => $faker->company, + 'vat_number' => 'FR' . $faker->numberBetween(10000000000, 99999999999), + 'siret' => $faker->numberBetween(10000000000000, 99999999999999), + 'email' => $faker->unique()->companyEmail, + 'phone' => $faker->phoneNumber, + + 'billing_address_line1' => $faker->streetAddress, + 'billing_address_line2' => $faker->optional()->secondaryAddress, + 'billing_postal_code' => $faker->postcode, + 'billing_city' => $faker->city, + 'billing_country_code' => 'FR', + + 'group_id' => null, // ou random si tu veux + 'notes' => $faker->optional()->sentence, + + 'is_active' => $faker->boolean(90), + 'is_parent' => false, + 'parent_id' => null, + + 'client_category_id' => $faker->numberBetween(1, 5), + + 'user_id' => 1, + + 'avatar' => null, + ]); + } + } +} \ No newline at end of file diff --git a/thanasoft-back/database/seeders/ContactSeeder.php b/thanasoft-back/database/seeders/ContactSeeder.php new file mode 100644 index 0000000..dec3faf --- /dev/null +++ b/thanasoft-back/database/seeders/ContactSeeder.php @@ -0,0 +1,43 @@ +orderBy('id')->get()->each(function (Client $client, int $index): void { + Contact::updateOrCreate( + [ + 'client_id' => $client->id, + 'email' => sprintf('contact.%d@clients.thanasoft.test', $client->id), + ], + [ + 'first_name' => 'Contact', + 'last_name' => sprintf('Client %d', $client->id), + 'phone' => sprintf('+26134000%04d', $client->id), + 'role' => $index % 2 === 0 ? 'Responsable funéraire' : 'Assistant administratif', + ] + ); + + if ($index < 8) { + Contact::updateOrCreate( + [ + 'client_id' => $client->id, + 'email' => sprintf('famille.%d@clients.thanasoft.test', $client->id), + ], + [ + 'first_name' => 'Référent', + 'last_name' => sprintf('Famille %d', $client->id), + 'phone' => sprintf('+26135000%04d', $client->id), + 'role' => 'Référent famille', + ] + ); + } + }); + } +} \ No newline at end of file diff --git a/thanasoft-back/database/seeders/DatabaseSeeder.php b/thanasoft-back/database/seeders/DatabaseSeeder.php index d01a0ef..34ec56d 100644 --- a/thanasoft-back/database/seeders/DatabaseSeeder.php +++ b/thanasoft-back/database/seeders/DatabaseSeeder.php @@ -15,9 +15,15 @@ class DatabaseSeeder extends Seeder { // User::factory(10)->create(); - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', - ]); + $this->call(AdminAccessSeeder::class); + + $this->call(ProductCategorySeeder::class); + $this->call(ProductSeeder::class); + $this->call(EmployeeSeeder::class); + $this->call(ThanatopractitionerSeeder::class); + $this->call(ClientSeeder::class); + $this->call(ContactSeeder::class); + $this->call(DeceasedSeeder::class); + $this->call(InterventionSeeder::class); } } diff --git a/thanasoft-back/database/seeders/DeceasedSeeder.php b/thanasoft-back/database/seeders/DeceasedSeeder.php new file mode 100644 index 0000000..5b68cb7 --- /dev/null +++ b/thanasoft-back/database/seeders/DeceasedSeeder.php @@ -0,0 +1,40 @@ + 'Martin', 'first_name' => 'Jeanne', 'birth_date' => '1942-03-11', 'death_date' => '2026-04-12', 'place_of_death' => 'Antananarivo'], + ['last_name' => 'Rabe', 'first_name' => 'Joseph', 'birth_date' => '1951-07-22', 'death_date' => '2026-04-10', 'place_of_death' => 'Fianarantsoa'], + ['last_name' => 'Rakoto', 'first_name' => 'Marie', 'birth_date' => '1938-12-01', 'death_date' => '2026-04-08', 'place_of_death' => 'Mahajanga'], + ['last_name' => 'Dubois', 'first_name' => 'Henri', 'birth_date' => '1947-09-14', 'death_date' => '2026-04-05', 'place_of_death' => 'Toamasina'], + ['last_name' => 'Rasolo', 'first_name' => 'Lucienne', 'birth_date' => '1955-11-09', 'death_date' => '2026-04-02', 'place_of_death' => 'Antsirabe'], + ['last_name' => 'Petit', 'first_name' => 'Alain', 'birth_date' => '1960-01-19', 'death_date' => '2026-03-30', 'place_of_death' => 'Antananarivo'], + ['last_name' => 'Andriamihaingo', 'first_name' => 'Suzanne', 'birth_date' => '1949-06-17', 'death_date' => '2026-03-27', 'place_of_death' => 'Moramanga'], + ['last_name' => 'Ranaivo', 'first_name' => 'Claude', 'birth_date' => '1958-08-05', 'death_date' => '2026-03-22', 'place_of_death' => 'Antananarivo'], + ['last_name' => 'Simon', 'first_name' => 'Madeleine', 'birth_date' => '1940-10-03', 'death_date' => '2026-03-19', 'place_of_death' => 'Antsiranana'], + ['last_name' => 'Raharisoa', 'first_name' => 'Paul', 'birth_date' => '1953-04-24', 'death_date' => '2026-03-15', 'place_of_death' => 'Toliara'], + ]; + + foreach ($records as $record) { + Deceased::updateOrCreate( + [ + 'last_name' => $record['last_name'], + 'first_name' => $record['first_name'], + 'death_date' => $record['death_date'], + ], + [ + 'birth_date' => $record['birth_date'], + 'place_of_death' => $record['place_of_death'], + 'notes' => 'Donnée de démonstration pour les interventions et le suivi défunt.', + ] + ); + } + } +} \ No newline at end of file diff --git a/thanasoft-back/database/seeders/EmployeeSeeder.php b/thanasoft-back/database/seeders/EmployeeSeeder.php new file mode 100644 index 0000000..812d3f8 --- /dev/null +++ b/thanasoft-back/database/seeders/EmployeeSeeder.php @@ -0,0 +1,134 @@ + 'Jean', + 'last_name' => 'Dupont', + 'email' => 'jean.dupont@thanasoft.com', + 'phone' => '+261341234567', + 'job_title' => 'Développeur Full-Stack', + 'hire_date' => '2022-01-15', + 'active' => true, + ], + [ + 'first_name' => 'Marie', + 'last_name' => 'Rasoa', + 'email' => 'marie.rasoa@thanasoft.com', + 'phone' => '+261341234569', + 'job_title' => 'Chef de Projet', + 'hire_date' => '2021-08-01', + 'active' => true, + ], + [ + 'first_name' => 'Paul', + 'last_name' => 'Ramanana', + 'email' => 'paul.ramanana@thanasoft.com', + 'phone' => '+261341234571', + 'job_title' => 'Designer UX/UI', + 'hire_date' => '2022-03-10', + 'active' => true, + ], + [ + 'first_name' => 'Sophie', + 'last_name' => 'Andriamatoa', + 'email' => 'sophie.andriamatoa@thanasoft.com', + 'phone' => '+261341234573', + 'job_title' => 'Responsable RH', + 'hire_date' => '2020-09-15', + 'active' => true, + ], + [ + 'first_name' => 'David', + 'last_name' => 'Randria', + 'email' => 'david.randria@thanasoft.com', + 'phone' => '+261341234575', + 'job_title' => 'Développeur Backend', + 'hire_date' => '2023-01-20', + 'active' => true, + ], + [ + 'first_name' => 'Lina', + 'last_name' => 'Ramaniraka', + 'email' => 'lina.ramaniraka@thanasoft.com', + 'phone' => '+261341234577', + 'job_title' => 'Comptable', + 'hire_date' => '2021-06-01', + 'active' => true, + ], + [ + 'first_name' => 'Marc', + 'last_name' => 'Andriantsoa', + 'email' => 'marc.andriantsoa@thanasoft.com', + 'phone' => '+261341234579', + 'job_title' => 'DevOps Engineer', + 'hire_date' => '2022-11-05', + 'active' => true, + ], + [ + 'first_name' => 'Julie', + 'last_name' => 'Rakotomalala', + 'email' => 'julie.rakotomalala@thanasoft.com', + 'phone' => '+261341234581', + 'job_title' => 'Community Manager', + 'hire_date' => '2023-03-15', + 'active' => true, + ], + [ + 'first_name' => 'Philippe', + 'last_name' => 'Rakoto', + 'email' => 'philippe.rakoto@thanasoft.com', + 'phone' => '+261341234583', + 'job_title' => 'Développeur Mobile', + 'hire_date' => '2023-06-10', + 'active' => true, + ], + [ + 'first_name' => 'Anne', + 'last_name' => 'Andriamanjato', + 'email' => 'anne.andriamanjato@thanasoft.com', + 'phone' => '+261341234585', + 'job_title' => 'Stagiaire', + 'hire_date' => '2024-01-15', + 'active' => true, + ], + ]; + + foreach ($employees as $employeeData) { + Employee::create($employeeData); + } + + // Create some inactive employees for testing + Employee::create([ + 'first_name' => 'Ancien', + 'last_name' => 'Employe', + 'email' => 'ancien.employe@thanasoft.com', + 'phone' => '+261341234587', + 'job_title' => 'Testeur', + 'hire_date' => '2020-01-01', + 'active' => false, + ]); + + Employee::create([ + 'first_name' => 'Employe', + 'last_name' => 'Suspendu', + 'email' => 'employe.suspendu@thanasoft.com', + 'phone' => '+261341234589', + 'job_title' => 'Assistant', + 'hire_date' => '2021-05-01', + 'active' => false, + ]); + } +} diff --git a/thanasoft-back/database/seeders/InterventionSeeder.php b/thanasoft-back/database/seeders/InterventionSeeder.php new file mode 100644 index 0000000..c6e2a52 --- /dev/null +++ b/thanasoft-back/database/seeders/InterventionSeeder.php @@ -0,0 +1,103 @@ +whereHas('category', fn ($query) => $query->where('intervention', true)) + ->get(); + $practitioners = Thanatopractitioner::query()->get(); + $clients = Client::query()->limit(12)->get(); + $creatorId = User::query()->value('id'); + $types = ['thanatopraxie', 'toilette_mortuaire', 'exhumation', 'retrait_pacemaker', 'retrait_bijoux', 'autre']; + $statuses = ['demande', 'planifie', 'en_cours', 'termine', 'annule']; + + if ($products->isEmpty() || $clients->isEmpty()) { + return; + } + + foreach ($clients as $index => $client) { + $deceased = Deceased::updateOrCreate( + [ + 'last_name' => sprintf('Défunt Client %d', $client->id), + 'first_name' => 'Dossier', + ], + [ + 'birth_date' => now()->subYears(55 + $index)->subDays($index)->format('Y-m-d'), + 'death_date' => now()->subDays($index + 1)->format('Y-m-d'), + 'place_of_death' => $client->billing_city ?: 'Antananarivo', + 'notes' => sprintf('Défunt de démonstration lié au client #%d pour les interventions seedées.', $client->id), + ] + ); + + $location = ClientLocation::updateOrCreate( + [ + 'client_id' => $client->id, + 'name' => 'Site principal', + ], + [ + 'address_line1' => $client->billing_address_line1, + 'address_line2' => $client->billing_address_line2, + 'postal_code' => $client->billing_postal_code, + 'city' => $client->billing_city, + 'country_code' => $client->billing_country_code ?: 'FR', + 'is_default' => true, + ] + ); + + $intervention = Intervention::updateOrCreate( + [ + 'client_id' => $client->id, + 'scheduled_at' => now()->subDays($index)->format('Y-m-d H:i:s'), + 'type' => $types[$index % count($types)], + ], + [ + 'deceased_id' => $deceased->id, + 'order_giver' => $faker->name, + 'location_id' => $location->id, + 'product_id' => optional($products->get($index % $products->count()))->id, + 'duration_min' => [60, 75, 90, 120][$index % 4], + 'status' => $statuses[$index % count($statuses)], + 'attachments_count' => 0, + 'notes' => $faker->sentence, + 'created_by' => $creatorId, + ] + ); + + if ($practitioners->isNotEmpty()) { + $principal = $practitioners->get($index % $practitioners->count()); + $assistant = $practitioners->get(($index + 1) % $practitioners->count()); + + $syncData = [ + $principal->id => [ + 'role' => 'principal', + 'assigned_at' => now()->subDays($index), + ], + ]; + + if ($assistant && $assistant->id !== $principal->id) { + $syncData[$assistant->id] = [ + 'role' => 'assistant', + 'assigned_at' => now()->subDays($index)->addMinutes(10), + ]; + } + + $intervention->practitioners()->sync($syncData); + } + } + } +} \ No newline at end of file diff --git a/thanasoft-back/database/seeders/ProductCategorySeeder.php b/thanasoft-back/database/seeders/ProductCategorySeeder.php new file mode 100644 index 0000000..3fe0252 --- /dev/null +++ b/thanasoft-back/database/seeders/ProductCategorySeeder.php @@ -0,0 +1,183 @@ + 'ELECTRONIQUE', + ], [ + 'name' => 'Électronique', + 'description' => 'Produits électroniques et technologiques', + 'intervention' => false, + 'active' => true, + ]); + + $alimentaire = ProductCategory::firstOrCreate([ + 'code' => 'ALIMENTAIRE', + ], [ + 'name' => 'Alimentaire', + 'description' => 'Produits alimentaires et boissons', + 'intervention' => false, + 'active' => true, + ]); + + $medical = ProductCategory::firstOrCreate([ + 'code' => 'MEDICAL', + ], [ + 'name' => 'Médical', + 'description' => 'Produits médicaux et pharmaceutiques', + 'intervention' => false, + 'active' => true, + ]); + + $interventions = ProductCategory::firstOrCreate([ + 'code' => 'INTERVENTIONS', + ], [ + 'name' => 'Interventions', + 'description' => 'Catégories de soins et prestations dédiées aux interventions funéraires.', + 'intervention' => true, + 'active' => true, + ]); + + $produits = ProductCategory::firstOrCreate([ + 'code' => 'PRODUITS', + ], [ + 'name' => 'Produits', + 'description' => 'Catégories de produits généraux et consommables.', + 'intervention' => false, + 'active' => true, + ]); + + // Create subcategories + ProductCategory::firstOrCreate([ + 'code' => 'TELEPHONE', + ], [ + 'parent_id' => $electronique->id, + 'name' => 'Téléphones', + 'description' => 'Smartphones et téléphones mobiles', + 'intervention' => false, + 'active' => true, + ]); + + ProductCategory::firstOrCreate([ + 'code' => 'ORDINATEUR', + ], [ + 'parent_id' => $electronique->id, + 'name' => 'Ordinateurs', + 'description' => 'Ordinateurs portables et de bureau', + 'intervention' => false, + 'active' => true, + ]); + + ProductCategory::firstOrCreate([ + 'code' => 'FRUITS', + ], [ + 'parent_id' => $alimentaire->id, + 'name' => 'Fruits', + 'description' => 'Fruits frais et transformés', + 'intervention' => false, + 'active' => true, + ]); + + ProductCategory::firstOrCreate([ + 'code' => 'LEGUMES', + ], [ + 'parent_id' => $alimentaire->id, + 'name' => 'Légumes', + 'description' => 'Légumes frais et transformés', + 'intervention' => false, + 'active' => true, + ]); + + ProductCategory::firstOrCreate([ + 'code' => 'MEDICAMENT', + ], [ + 'parent_id' => $medical->id, + 'name' => 'Médicaments', + 'description' => 'Médicaments et traitements', + 'intervention' => false, + 'active' => true, + ]); + + ProductCategory::firstOrCreate([ + 'code' => 'MATERIEL', + ], [ + 'parent_id' => $medical->id, + 'name' => 'Matériel Médical', + 'description' => 'Équipements et instruments médicaux', + 'intervention' => false, + 'active' => true, + ]); + + ProductCategory::firstOrCreate([ + 'code' => 'SOINS_THANATO', + ], [ + 'parent_id' => $interventions->id, + 'name' => 'Soins thanatopraxiques', + 'description' => 'Soins de conservation, présentation et préparation du défunt.', + 'intervention' => true, + 'active' => true, + ]); + + ProductCategory::firstOrCreate([ + 'code' => 'PREPARATION_DEFUNT', + ], [ + 'parent_id' => $interventions->id, + 'name' => 'Préparation du défunt', + 'description' => 'Toilette mortuaire, habillage et préparation avant cérémonie.', + 'intervention' => true, + 'active' => true, + ]); + + ProductCategory::firstOrCreate([ + 'code' => 'ACCESSOIRES_FUNERAIRES', + ], [ + 'parent_id' => $produits->id, + 'name' => 'Accessoires funéraires', + 'description' => 'Consommables et accessoires utilisés lors des prestations funéraires.', + 'intervention' => false, + 'active' => true, + ]); + + ProductCategory::firstOrCreate([ + 'code' => 'CONSOMMABLES_SOINS', + ], [ + 'parent_id' => $produits->id, + 'name' => 'Consommables de soins', + 'description' => 'Produits consommables utilisés pendant les soins d\'intervention.', + 'intervention' => false, + 'active' => true, + ]); + + // Create some inactive categories for testing + ProductCategory::firstOrCreate([ + 'code' => 'COSMETIQUE', + ], [ + 'code' => 'COSMETIQUE', + 'name' => 'Cosmétique', + 'description' => 'Produits cosmétiques et de beauté', + 'intervention' => false, + 'active' => false, + ]); + + ProductCategory::firstOrCreate([ + 'code' => 'MENAGE', + ], [ + 'code' => 'MENAGE', + 'name' => 'Ménage', + 'description' => 'Produits d\'entretien et de nettoyage', + 'intervention' => false, + 'active' => false, + ]); + } +} diff --git a/thanasoft-back/database/seeders/ProductSeeder.php b/thanasoft-back/database/seeders/ProductSeeder.php new file mode 100644 index 0000000..a2fe0b3 --- /dev/null +++ b/thanasoft-back/database/seeders/ProductSeeder.php @@ -0,0 +1,121 @@ + 'Soin de thanatopraxie standard', + 'reference' => 'INT-THAN-001', + 'category_code' => 'SOINS_THANATO', + 'fabricant' => 'Thanasoft Care', + 'stock_actuel' => 50, + 'stock_minimum' => 10, + 'unite' => 'prestation', + 'prix_unitaire' => 320.00, + 'conditionnement_nom' => 'Unité', + 'conditionnement_quantite' => 1, + 'conditionnement_unite' => 'prestation', + ], + [ + 'nom' => 'Toilette mortuaire complète', + 'reference' => 'INT-TOIL-001', + 'category_code' => 'PREPARATION_DEFUNT', + 'fabricant' => 'Thanasoft Care', + 'stock_actuel' => 40, + 'stock_minimum' => 8, + 'unite' => 'prestation', + 'prix_unitaire' => 180.00, + 'conditionnement_nom' => 'Unité', + 'conditionnement_quantite' => 1, + 'conditionnement_unite' => 'prestation', + ], + [ + 'nom' => 'Retrait pacemaker', + 'reference' => 'INT-PACE-001', + 'category_code' => 'PREPARATION_DEFUNT', + 'fabricant' => 'Thanasoft Care', + 'stock_actuel' => 20, + 'stock_minimum' => 4, + 'unite' => 'prestation', + 'prix_unitaire' => 95.00, + 'conditionnement_nom' => 'Unité', + 'conditionnement_quantite' => 1, + 'conditionnement_unite' => 'prestation', + ], + [ + 'nom' => 'Kit de soins de conservation', + 'reference' => 'PROD-SOIN-001', + 'category_code' => 'CONSOMMABLES_SOINS', + 'fabricant' => 'Mortuary Supply', + 'stock_actuel' => 120, + 'stock_minimum' => 25, + 'unite' => 'kit', + 'prix_unitaire' => 42.50, + 'conditionnement_nom' => 'Carton', + 'conditionnement_quantite' => 10, + 'conditionnement_unite' => 'kit', + ], + [ + 'nom' => 'Housse funéraire premium', + 'reference' => 'PROD-HOUS-001', + 'category_code' => 'ACCESSOIRES_FUNERAIRES', + 'fabricant' => 'Funeral Equip', + 'stock_actuel' => 75, + 'stock_minimum' => 15, + 'unite' => 'unité', + 'prix_unitaire' => 28.90, + 'conditionnement_nom' => 'Paquet', + 'conditionnement_quantite' => 5, + 'conditionnement_unite' => 'unité', + ], + [ + 'nom' => 'Produit de désinfection mortuaire', + 'reference' => 'PROD-DES-001', + 'category_code' => 'CONSOMMABLES_SOINS', + 'fabricant' => 'Mortuary Supply', + 'stock_actuel' => 90, + 'stock_minimum' => 20, + 'unite' => 'litre', + 'prix_unitaire' => 14.75, + 'conditionnement_nom' => 'Bidon', + 'conditionnement_quantite' => 5, + 'conditionnement_unite' => 'litre', + ], + ]; + + foreach ($products as $data) { + $category = ProductCategory::where('code', $data['category_code'])->first(); + + if (! $category) { + continue; + } + + Product::updateOrCreate( + ['reference' => $data['reference']], + [ + 'nom' => $data['nom'], + 'categorie_id' => $category->id, + 'fabricant' => $data['fabricant'], + 'stock_actuel' => $data['stock_actuel'], + 'stock_minimum' => $data['stock_minimum'], + 'unite' => $data['unite'], + 'prix_unitaire' => $data['prix_unitaire'], + 'conditionnement_nom' => $data['conditionnement_nom'], + 'conditionnement_quantite' => $data['conditionnement_quantite'], + 'conditionnement_unite' => $data['conditionnement_unite'], + 'date_expiration' => null, + 'numero_lot' => null, + 'fournisseur_id' => null, + ] + ); + } + } +} \ No newline at end of file diff --git a/thanasoft-back/database/seeders/ThanatopractitionerSeeder.php b/thanasoft-back/database/seeders/ThanatopractitionerSeeder.php new file mode 100644 index 0000000..2b7b361 --- /dev/null +++ b/thanasoft-back/database/seeders/ThanatopractitionerSeeder.php @@ -0,0 +1,277 @@ + 'Jean-Baptiste', + 'last_name' => 'Ramanana', + 'email' => 'jb.ramanana@thanasoft.com', + 'phone' => '+261341235001', + 'job_title' => 'Thanatopracteur Senior', + 'hire_date' => '2020-08-01', + 'active' => true, + ], + [ + 'first_name' => 'Marie', + 'last_name' => 'Andriamanantsoa', + 'email' => 'marie.andriamanantsoa@thanasoft.com', + 'phone' => '+261341235003', + 'job_title' => 'Thanatopracteur Spécialisé', + 'hire_date' => '2019-07-01', + 'active' => true, + ], + [ + 'first_name' => 'Paul', + 'last_name' => 'Rakotomanga', + 'email' => 'paul.rakotomanga@thanasoft.com', + 'phone' => '+261341235005', + 'job_title' => 'Thanatopracteur Junior', + 'hire_date' => '2021-11-01', + 'active' => true, + ], + [ + 'first_name' => 'Sophie', + 'last_name' => 'Andriatiana', + 'email' => 'sophie.andriatiana@thanasoft.com', + 'phone' => '+261341235007', + 'job_title' => 'Thanatopracteur Expert', + 'hire_date' => '2018-06-01', + 'active' => true, + ], + [ + 'first_name' => 'David', + 'last_name' => 'Randriamahandry', + 'email' => 'david.randriamahandry@thanasoft.com', + 'phone' => '+261341235009', + 'job_title' => 'Thanatopracteur', + 'hire_date' => '2022-03-01', + 'active' => true, + ], + [ + 'first_name' => 'Lina', + 'last_name' => 'Ramaniraka', + 'email' => 'lina.ramaniraka@thanasoft.com', + 'phone' => '+261341235011', + 'job_title' => 'Thanatopracteur', + 'hire_date' => '2023-10-01', + 'active' => true, + ], + [ + 'first_name' => 'Marc', + 'last_name' => 'Andriamatsiroa', + 'email' => 'marc.andriamatsiroa@thanasoft.com', + 'phone' => '+261341235013', + 'job_title' => 'Thanatopracteur Chief', + 'hire_date' => '2018-01-01', + 'active' => true, + ], + [ + 'first_name' => 'Julie', + 'last_name' => 'Rakotomalala', + 'email' => 'julie.rakotomalala@thanasoft.com', + 'phone' => '+261341235015', + 'job_title' => 'Thanatopracteur Assistant', + 'hire_date' => '2024-05-01', + 'active' => true, + ], + ]; + + // Create employees and get their IDs + $employeeIds = []; + foreach ($employees as $employeeData) { + $employee = \App\Models\Employee::create($employeeData); + $employeeIds[] = $employee->id; + } + + // Create thanatopractitioners linked to the employees + $thanatopractitioners = [ + [ + 'employee_id' => $employeeIds[0], + 'diploma_number' => 'TP-DIPL-001-2020', + 'diploma_date' => '2020-06-15', + 'authorization_number' => 'TP-AUTH-001-2020', + 'authorization_issue_date' => '2020-07-01', + 'authorization_expiry_date' => '2025-07-01', + 'notes' => 'Thanatopracteur senior spécialisé en thanatopraxie générale et reconstructive.', + ], + [ + 'employee_id' => $employeeIds[1], + 'diploma_number' => 'TP-DIPL-002-2019', + 'diploma_date' => '2019-05-10', + 'authorization_number' => 'TP-AUTH-002-2019', + 'authorization_issue_date' => '2019-06-01', + 'authorization_expiry_date' => '2024-06-01', + 'notes' => 'Spécialiste en thanatopraxie pédiatrique avec 5 ans d\'expérience.', + ], + [ + 'employee_id' => $employeeIds[2], + 'diploma_number' => 'TP-DIPL-003-2021', + 'diploma_date' => '2021-09-22', + 'authorization_number' => 'TP-AUTH-003-2021', + 'authorization_issue_date' => '2021-10-01', + 'authorization_expiry_date' => '2026-10-01', + 'notes' => 'Thanatopracteur junior spécialisé en thanatopraxie esthétique.', + ], + [ + 'employee_id' => $employeeIds[3], + 'diploma_number' => 'TP-DIPL-004-2018', + 'diploma_date' => '2018-04-05', + 'authorization_number' => 'TP-AUTH-004-2018', + 'authorization_issue_date' => '2018-05-01', + 'authorization_expiry_date' => '2023-05-01', + 'notes' => 'Expert en thanatopraxie reconstructive et histopathologie.', + ], + [ + 'employee_id' => $employeeIds[4], + 'diploma_number' => 'TP-DIPL-005-2022', + 'diploma_date' => '2022-01-18', + 'authorization_number' => 'TP-AUTH-005-2022', + 'authorization_issue_date' => '2022-02-01', + 'authorization_expiry_date' => '2027-02-01', + 'notes' => 'Spécialiste en thanatopraxie traditionnelle et culturelle.', + ], + [ + 'employee_id' => $employeeIds[5], + 'diploma_number' => 'TP-DIPL-006-2023', + 'diploma_date' => '2023-08-12', + 'authorization_number' => 'TP-AUTH-006-2023', + 'authorization_issue_date' => '2023-09-01', + 'authorization_expiry_date' => '2028-09-01', + 'notes' => 'Thanatopracteur nouvellement certifiée, spécialisée en techniques modernes.', + ], + [ + 'employee_id' => $employeeIds[6], + 'diploma_number' => 'TP-DIPL-007-2017', + 'diploma_date' => '2017-11-30', + 'authorization_number' => 'TP-AUTH-007-2017', + 'authorization_issue_date' => '2018-01-01', + 'authorization_expiry_date' => '2023-01-01', + 'notes' => 'Responsable principal des thanatopracteurs, expert en histopathologie.', + ], + [ + 'employee_id' => $employeeIds[7], + 'diploma_number' => 'TP-DIPL-008-2024', + 'diploma_date' => '2024-03-25', + 'authorization_number' => 'TP-AUTH-008-2024', + 'authorization_issue_date' => '2024-04-01', + 'authorization_expiry_date' => '2029-04-01', + 'notes' => 'Thanatopracteur assistante, en formation continue en thanatopraxie assistée.', + ], + ]; + + foreach ($thanatopractitioners as $thanatopractitionerData) { + $thanatopractitioner = Thanatopractitioner::create($thanatopractitionerData); + + // Create some practitioner documents for each thanatopractitioner + $this->createPractitionerDocuments($thanatopractitioner); + } + + // Create inactive thanatopractitioners for testing + $inactiveEmployee1 = \App\Models\Employee::create([ + 'first_name' => 'Ancien', + 'last_name' => 'Thanatopracteur', + 'email' => 'ancien.thanato@thanasoft.com', + 'phone' => '+261341235017', + 'job_title' => 'Ancien Thanatopracteur', + 'hire_date' => '2015-03-01', + 'active' => false, + ]); + + $inactive1 = Thanatopractitioner::create([ + 'employee_id' => $inactiveEmployee1->id, + 'diploma_number' => 'TP-DIPL-TEST-001', + 'diploma_date' => '2015-01-01', + 'authorization_number' => 'TP-AUTH-TEST-001', + 'authorization_issue_date' => '2015-02-01', + 'authorization_expiry_date' => '2020-02-01', + 'notes' => 'Thanatopracteur inactif, autorisation expirée.', + ]); + + $this->createPractitionerDocuments($inactive1); + + $inactiveEmployee2 = \App\Models\Employee::create([ + 'first_name' => 'Thanatopracteur', + 'last_name' => 'Suspendu', + 'email' => 'thanato.suspendu@thanasoft.com', + 'phone' => '+261341235019', + 'job_title' => 'Thanatopracteur Suspendu', + 'hire_date' => '2018-08-01', + 'active' => false, + ]); + + $inactive2 = Thanatopractitioner::create([ + 'employee_id' => $inactiveEmployee2->id, + 'diploma_number' => 'TP-DIPL-TEST-002', + 'diploma_date' => '2018-06-01', + 'authorization_number' => 'TP-AUTH-TEST-002', + 'authorization_issue_date' => '2018-07-01', + 'authorization_expiry_date' => '2023-07-01', + 'notes' => 'Thanatopracteur temporairement suspendu.', + ]); + + $this->createPractitionerDocuments($inactive2); + } + + /** + * Create practitioner documents for a thanatopractitioner. + */ + private function createPractitionerDocuments(Thanatopractitioner $thanatopractitioner): void + { + $documents = [ + [ + 'doc_type' => 'diploma', + 'issue_date' => $thanatopractitioner->diploma_date, + 'expiry_date' => date('Y-m-d', strtotime('+5 years', strtotime($thanatopractitioner->diploma_date))), + 'status' => 'active', + ], + [ + 'doc_type' => 'certification', + 'issue_date' => date('Y-01-01'), + 'expiry_date' => date('Y-12-31'), + 'status' => 'active', + ], + ]; + + // Add specialized documents based on notes content + if (strpos($thanatopractitioner->notes, 'reconstructive') !== false || + strpos($thanatopractitioner->notes, 'histopathologie') !== false) { + $documents[] = [ + 'doc_type' => 'specialization', + 'issue_date' => $thanatopractitioner->diploma_date, + 'expiry_date' => date('Y-m-d', strtotime('+3 years', strtotime($thanatopractitioner->diploma_date))), + 'status' => 'active', + ]; + } + + // Add expired documents for inactive thanatopractitioners + $employee = $thanatopractitioner->employee; + if ($employee->active === false) { + $documents[] = [ + 'doc_type' => 'expired_certificate', + 'issue_date' => '2020-01-01', + 'expiry_date' => '2023-01-01', + 'status' => 'expired', + ]; + } + + foreach ($documents as $documentData) { + PractitionerDocument::create(array_merge($documentData, [ + 'practitioner_id' => $thanatopractitioner->id, + 'file_id' => null, // Will be set when files table is implemented + ])); + } + } +} diff --git a/thanasoft-back/resources/views/emails/document.blade.php b/thanasoft-back/resources/views/emails/document.blade.php new file mode 100644 index 0000000..ba00eff --- /dev/null +++ b/thanasoft-back/resources/views/emails/document.blade.php @@ -0,0 +1,39 @@ + + + + + + + +
+
+

Thanasoft

+
+ +

Bonjour {{ $document->client->name }},

+ + @if($type === 'quote') +

Veuillez trouver ci-joint le devis {{ $document->reference }} suite à votre demande.

+

Ce devis est valable jusqu'au {{ $document->valid_until->format('d/m/Y') }}.

+ @else +

Veuillez trouver ci-joint la facture {{ $document->invoice_number }} d'un montant de {{ number_format($document->total_ttc, 2, ',', ' ') }} {{ $document->currency }}.

+

La date d'échéance est fixée au {{ $document->due_date->format('d/m/Y') }}.

+ @endif + +

N'hésitez pas à nous contacter pour toute question supplémentaire.

+ +

Cordialement,
+ L'équipe Thanasoft

+ + +
+ + diff --git a/thanasoft-back/resources/views/emails/webmail_message.blade.php b/thanasoft-back/resources/views/emails/webmail_message.blade.php new file mode 100644 index 0000000..d1bc994 --- /dev/null +++ b/thanasoft-back/resources/views/emails/webmail_message.blade.php @@ -0,0 +1,13 @@ + + + + + + Email + + +
+ {!! $body !!} +
+ + \ No newline at end of file diff --git a/thanasoft-back/resources/views/pdf/invoice_pdf.blade.php b/thanasoft-back/resources/views/pdf/invoice_pdf.blade.php new file mode 100644 index 0000000..bd28f78 --- /dev/null +++ b/thanasoft-back/resources/views/pdf/invoice_pdf.blade.php @@ -0,0 +1,73 @@ + + + + + Facture {{ $invoice->invoice_number }} + + + +
+
+

THANASOFT

+

Solutions de gestion funéraire

+
+
+

CLIENT

+

{{ $invoice->client->name }}

+

{{ $invoice->client->billing_address_line1 }}

+

{{ $invoice->client->billing_postal_code }} {{ $invoice->client->billing_city }}

+
+
+
+ +
Facture : {{ $invoice->invoice_number }}
+

Date : {{ $invoice->invoice_date->format('d/m/Y') }}
+ Échéance : {{ $invoice->due_date->format('d/m/Y') }}

+ + + + + + + + + + + + @foreach($invoice->lines as $line) + + + + + + + @endforeach + +
DescriptionQuantitéPrix UnitaireTotal HT
{{ $line->description }}{{ $line->quantity }}{{ number_format($line->unit_price, 2, ',', ' ') }} {{ $invoice->currency }}{{ number_format($line->total_ht, 2, ',', ' ') }} {{ $invoice->currency }}
+ +
+
Total HT : {{ number_format($invoice->total_ht, 2, ',', ' ') }} {{ $invoice->currency }}
+
TVA : {{ number_format($invoice->total_tva, 2, ',', ' ') }} {{ $invoice->currency }}
+
TOTAL TTC : {{ number_format($invoice->total_ttc, 2, ',', ' ') }} {{ $invoice->currency }}
+
+ + + + diff --git a/thanasoft-back/resources/views/pdf/quote_pdf.blade.php b/thanasoft-back/resources/views/pdf/quote_pdf.blade.php new file mode 100644 index 0000000..98376fd --- /dev/null +++ b/thanasoft-back/resources/views/pdf/quote_pdf.blade.php @@ -0,0 +1,75 @@ + + + + + Devis {{ $quote->reference }} + + + +
+
+

THANASOFT

+

Solutions de gestion funéraire

+
+
+

{{ $quote->client ? 'CLIENT' : 'GROUPE CLIENT' }}

+

{{ $quote->client?->name ?? $quote->group?->name ?? 'Destinataire inconnu' }}

+ @if($quote->client) +

{{ $quote->client->billing_address_line1 }}

+

{{ $quote->client->billing_postal_code }} {{ $quote->client->billing_city }}

+ @endif +
+
+
+ +
Devis : {{ $quote->reference }}
+

Date : {{ $quote->quote_date->format('d/m/Y') }}
+ Valable jusqu'au : {{ $quote->valid_until?->format('d/m/Y') ?? 'Non definie' }}

+ + + + + + + + + + + + @foreach($quote->lines as $line) + + + + + + + @endforeach + +
DescriptionQuantitéPrix UnitaireTotal HT
{{ $line->description }}{{ $line->quantity ?? $line->units_qty ?? $line->qty_base ?? 0 }}{{ number_format($line->unit_price, 2, ',', ' ') }} {{ $quote->currency }}{{ number_format($line->total_ht, 2, ',', ' ') }} {{ $quote->currency }}
+ +
+
Total HT : {{ number_format($quote->total_ht, 2, ',', ' ') }} {{ $quote->currency }}
+
TVA : {{ number_format($quote->total_tva, 2, ',', ' ') }} {{ $quote->currency }}
+
TOTAL TTC : {{ number_format($quote->total_ttc, 2, ',', ' ') }} {{ $quote->currency }}
+
+ + + + diff --git a/thanasoft-back/routes/api.php b/thanasoft-back/routes/api.php index 62d4673..e2ef0c2 100644 --- a/thanasoft-back/routes/api.php +++ b/thanasoft-back/routes/api.php @@ -2,6 +2,34 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\Api\AuthController; +use App\Http\Controllers\Api\ClientController; +use App\Http\Controllers\Api\ClientGroupController; +use App\Http\Controllers\Api\ClientLocationController; +use App\Http\Controllers\Api\ContactController; +use App\Http\Controllers\Api\ClientCategoryController; +use App\Http\Controllers\Api\FournisseurController; +use App\Http\Controllers\Api\ProductController; +use App\Http\Controllers\Api\ProductCategoryController; +use App\Http\Controllers\Api\EmployeeController; +use App\Http\Controllers\Api\ThanatopractitionerController; +use App\Http\Controllers\Api\PractitionerDocumentController; +use App\Http\Controllers\Api\DeceasedController; +use App\Http\Controllers\Api\DeceasedDocumentController; +use App\Http\Controllers\Api\InterventionController; +use App\Http\Controllers\Api\FileController; +use App\Http\Controllers\Api\FileAttachmentController; +use App\Http\Controllers\Api\QuoteController; +use App\Http\Controllers\Api\ClientActivityTimelineController; +use App\Http\Controllers\Api\AccessControlController; +use App\Http\Controllers\Api\PurchaseOrderController; +use App\Http\Controllers\Api\PriceListController; +use App\Http\Controllers\Api\TvaRateController; +use App\Http\Controllers\Api\GoodsReceiptController; +use App\Http\Controllers\Api\UserController; +use App\Http\Controllers\Api\VehicleController; +use App\Http\Controllers\Api\ConvoyController; +use App\Http\Controllers\Api\WebmailController; + /* |-------------------------------------------------------------------------- @@ -16,6 +44,8 @@ use App\Http\Controllers\Api\AuthController; Route::prefix('auth')->group(function () { Route::post('/register', [AuthController::class, 'register']); Route::post('/login', [AuthController::class, 'login']); + Route::post('/check-email', [AuthController::class, 'checkEmail']); + Route::post('/create-password', [AuthController::class, 'createPasswordAndLogin']); Route::middleware('auth:sanctum')->group(function () { Route::get('/me', [AuthController::class, 'me']); @@ -25,3 +55,193 @@ Route::prefix('auth')->group(function () { Route::post('/logout-all', [AuthController::class, 'logoutAll']); }); }); + +// Protected API routes +Route::middleware('auth:sanctum')->group(function () { + // Client management + // IMPORTANT: Specific routes must come before apiResource + Route::get('/clients/searchBy', [ClientController::class, 'searchBy']); + + Route::apiResource('clients', ClientController::class); + Route::post('client-groups/{id}/assign-clients', [ClientGroupController::class, 'assignClients']); + Route::apiResource('client-groups', ClientGroupController::class); + Route::apiResource('price-lists', PriceListController::class); + Route::apiResource('users', UserController::class); + Route::prefix('webmail')->group(function () { + Route::get('settings', [WebmailController::class, 'mailboxSettings']); + Route::put('settings', [WebmailController::class, 'upsertMailboxSettings']); + Route::post('settings/test-smtp', [WebmailController::class, 'testSmtp']); + Route::get('messages/stats', [WebmailController::class, 'stats']); + Route::get('messages', [WebmailController::class, 'index']); + Route::post('messages/send', [WebmailController::class, 'send']); + Route::post('messages/receive', [WebmailController::class, 'receive']); + Route::post('messages/sync', [WebmailController::class, 'sync']); + Route::post('messages/sync-mailtrap', [WebmailController::class, 'syncMailtrap']); + Route::get('messages/{id}', [WebmailController::class, 'show']); + Route::patch('messages/{id}', [WebmailController::class, 'update']); + Route::delete('messages/{id}', [WebmailController::class, 'destroy']); + }); + Route::middleware('permission:config.view_roles')->group(function () { + Route::get('access-control', [AccessControlController::class, 'index']); + Route::post('access-control/roles', [AccessControlController::class, 'storeRole']); + Route::put('access-control/roles/{id}', [AccessControlController::class, 'updateRole']); + Route::delete('access-control/roles/{id}', [AccessControlController::class, 'destroyRole']); + Route::put('access-control/roles/{id}/permissions', [AccessControlController::class, 'syncRolePermissions']); + Route::post('access-control/permissions', [AccessControlController::class, 'storePermission']); + Route::put('access-control/permissions/{id}', [AccessControlController::class, 'updatePermission']); + Route::delete('access-control/permissions/{id}', [AccessControlController::class, 'destroyPermission']); + }); + + Route::apiResource('client-locations', ClientLocationController::class); + Route::apiResource('client-locations', ClientLocationController::class); + Route::get('clients/{clientId}/locations', [ClientLocationController::class, 'getLocationsByClient']); + + // Client Parent/Child routes + Route::get('clients/{id}/children', [ClientController::class, 'getChildren']); + Route::post('clients/{id}/children/{childId}', [ClientController::class, 'addChild']); + Route::delete('clients/{id}/children/{childId}', [ClientController::class, 'removeChild']); + Route::patch('clients/{id}/status', [ClientController::class, 'changeStatus']); + Route::get('clients/{client}/timeline', [ClientActivityTimelineController::class, 'index']); + + // Contact management + Route::apiResource('contacts', ContactController::class); + Route::get('clients/{clientId}/contacts', [ContactController::class, 'getContactsByClient']); + + Route::apiResource('client-categories', ClientCategoryController::class); + + // Quote management + Route::post('/quotes/{id}/send-email', [QuoteController::class, 'sendByEmail']); + Route::get('/quotes/{id}/download-pdf', [QuoteController::class, 'downloadPdf']); + Route::apiResource('quotes', QuoteController::class); + + // Invoice management + Route::post('/invoices/{id}/send-email', [\App\Http\Controllers\Api\InvoiceController::class, 'sendByEmail']); + Route::post('/invoices/from-quote/{quoteId}', [\App\Http\Controllers\Api\InvoiceController::class, 'createFromQuote']); + Route::apiResource('invoices', \App\Http\Controllers\Api\InvoiceController::class); + + // Avoir management + Route::apiResource('avoirs', \App\Http\Controllers\Api\AvoirController::class); + + // Fournisseur management + Route::get('/fournisseurs/searchBy', [FournisseurController::class, 'searchBy']); + Route::apiResource('purchase-orders', PurchaseOrderController::class); + Route::apiResource('fournisseurs', FournisseurController::class); + Route::get('fournisseurs/{fournisseurId}/contacts', [ContactController::class, 'getContactsByFournisseur']); + + // Product management + Route::get('/products/searchBy', [ProductController::class, 'searchBy']); + Route::get('/products/low-stock', [ProductController::class, 'lowStock']); + Route::get('/products/by-category', [ProductController::class, 'byCategory']); + Route::get('/products/statistics', [ProductController::class, 'statistics']); + Route::apiResource('products', ProductController::class); + Route::patch('/products/{id}/stock', [ProductController::class, 'updateStock']); + + // Warehouse management + Route::get('/warehouses/searchBy', [\App\Http\Controllers\Api\WarehouseController::class, 'searchBy']); + Route::apiResource('warehouses', \App\Http\Controllers\Api\WarehouseController::class); + + // Stock management + Route::apiResource('stock-items', \App\Http\Controllers\Api\StockItemController::class); + Route::apiResource('stock-moves', \App\Http\Controllers\Api\StockMoveController::class); + + // TVA Rates management + Route::apiResource('tva-rates', TvaRateController::class); + + // Goods Receipts management + Route::apiResource('goods-receipts', GoodsReceiptController::class); + Route::apiResource('vehicles', VehicleController::class); + Route::apiResource('convoys', ConvoyController::class); + + // Product Category management + Route::get('/product-categories/search', [ProductCategoryController::class, 'search']); + Route::get('/product-categories/active', [ProductCategoryController::class, 'active']); + Route::get('/product-categories/roots', [ProductCategoryController::class, 'roots']); + Route::get('/product-categories/hierarchical', [ProductCategoryController::class, 'hierarchical']); + Route::get('/product-categories/statistics', [ProductCategoryController::class, 'statistics']); + Route::apiResource('product-categories', ProductCategoryController::class); + Route::patch('/product-categories/{id}/toggle-active', [ProductCategoryController::class, 'toggleActive']); + + // Employee management + Route::get('/employees/searchBy', [EmployeeController::class, 'searchBy']); + Route::get('/employees/thanatopractitioners', [EmployeeController::class, 'getThanatopractitioners']); + Route::get('/employees/{id}/agenda', [EmployeeController::class, 'agenda']); + Route::apiResource('employees', EmployeeController::class); + + // Thanatopractitioner management + Route::get('/thanatopractitioners/search', [ThanatopractitionerController::class, 'searchByEmployeeName']); + Route::apiResource('thanatopractitioners', ThanatopractitionerController::class); + Route::get('employees/{employeeId}/thanatopractitioners', [ThanatopractitionerController::class, 'getByEmployee']); + Route::get('/thanatopractitioners/{id}/documents', [PractitionerDocumentController::class, 'getByThanatopractitioner']); + + // Practitioner Document management + Route::get('/practitioner-documents/searchBy', [PractitionerDocumentController::class, 'searchBy']); + Route::get('/practitioner-documents/expiring', [PractitionerDocumentController::class, 'getExpiringDocuments']); + Route::apiResource('practitioner-documents', PractitionerDocumentController::class); + Route::patch('/practitioner-documents/{id}/verify', [PractitionerDocumentController::class, 'verifyDocument']); + + // Deceased Routes + Route::prefix('deceased')->group(function () { + Route::get('/searchBy', [DeceasedController::class, 'searchBy']); + Route::get('/', [DeceasedController::class, 'index']); + Route::post('/', [DeceasedController::class, 'store']); + Route::get('/{deceased}', [DeceasedController::class, 'show']); + Route::put('/{deceased}', [DeceasedController::class, 'update']); + Route::delete('/{deceased}', [DeceasedController::class, 'destroy']); + }); + + // Deceased Document Routes + Route::prefix('deceased-documents')->group(function () { + Route::get('/by-deceased/{deceasedId}', [DeceasedDocumentController::class, 'byDeceased']); + Route::get('/by-doc-type', [DeceasedDocumentController::class, 'byDocType']); + Route::get('/by-file/{fileId}', [DeceasedDocumentController::class, 'byFile']); + Route::get('/search', [DeceasedDocumentController::class, 'search']); + Route::get('/', [DeceasedDocumentController::class, 'index']); + Route::post('/', [DeceasedDocumentController::class, 'store']); + Route::get('/{id}', [DeceasedDocumentController::class, 'show']); + Route::put('/{id}', [DeceasedDocumentController::class, 'update']); + Route::delete('/{id}', [DeceasedDocumentController::class, 'destroy']); + }); + + // Intervention Routes + Route::prefix('interventions')->group(function () { + Route::get('/by-month', [InterventionController::class, 'byMonth']); + Route::get('/', [InterventionController::class, 'index']); + Route::post('/', [InterventionController::class, 'store']); + Route::post('/with-all-data', [InterventionController::class, 'createInterventionalldata']); + Route::get('/{intervention}', [InterventionController::class, 'show']); + Route::put('/{intervention}', [InterventionController::class, 'update']); + Route::delete('/{intervention}', [InterventionController::class, 'destroy']); + Route::patch('/{intervention}/status', [InterventionController::class, 'changeStatus']); + Route::patch('/{intervention}/assign', [InterventionController::class, 'createAssignment']); + Route::patch('/{intervention}/{practitionerId}/unassignPractitioner', [InterventionController::class, 'unassignPractitioner']); + Route::get('/{intervention}/debug', [InterventionController::class, 'debugPractitioners']); + }); + + // File management + Route::prefix('files')->group(function () { + Route::get('/', [FileController::class, 'index']); + Route::post('/', [FileController::class, 'store']); + Route::get('/by-category/{category}', [FileController::class, 'byCategory']); + Route::get('/by-client/{clientId}', [FileController::class, 'byClient']); + Route::get('/organized', [FileController::class, 'organized']); + Route::get('/statistics', [FileController::class, 'stats']); + Route::get('/{id}', [FileController::class, 'show']); + Route::put('/{id}', [FileController::class, 'update']); + Route::delete('/{id}', [FileController::class, 'destroy']); + Route::get('/{id}/download', [FileController::class, 'download']); + }); + + // File Attachment management + Route::prefix('file-attachments')->group(function () { + Route::post('/', [FileAttachmentController::class, 'attach']); + Route::put('/{attachmentId}', [FileAttachmentController::class, 'update']); + Route::delete('/{attachmentId}', [FileAttachmentController::class, 'detach']); + Route::post('/detach-multiple', [FileAttachmentController::class, 'detachMultiple']); + Route::post('/reorder', [FileAttachmentController::class, 'reorder']); + Route::get('/attached-files', [FileAttachmentController::class, 'getAttachedFiles']); + Route::get('/intervention/{interventionId}/files', [FileAttachmentController::class, 'getInterventionFiles']); + Route::get('/client/{clientId}/files', [FileAttachmentController::class, 'getClientFiles']); + Route::get('/deceased/{deceasedId}/files', [FileAttachmentController::class, 'getDeceasedFiles']); + }); + +}); diff --git a/thanasoft-back/routes/web.php b/thanasoft-back/routes/web.php index 46c2d68..999b0f0 100644 --- a/thanasoft-back/routes/web.php +++ b/thanasoft-back/routes/web.php @@ -3,5 +3,9 @@ use Illuminate\Support\Facades\Route; Route::get('/{any}', function () { - return file_get_contents(public_path('build/index.html')); + $path = public_path('build/index.html'); + if (!file_exists($path)) { + return response('Frontend build not found. Please run "npm run build" in the thanasoft-front directory.', 404); + } + return file_get_contents($path); })->where('any', '.*'); diff --git a/thanasoft-back/routes/web.php.server b/thanasoft-back/routes/web.php.server deleted file mode 100644 index 856b450..0000000 --- a/thanasoft-back/routes/web.php.server +++ /dev/null @@ -1,7 +0,0 @@ -where('any', '.*'); diff --git a/thanasoft-back/tests/Feature/EmployeeAgendaApiTest.php b/thanasoft-back/tests/Feature/EmployeeAgendaApiTest.php new file mode 100644 index 0000000..6d5920b --- /dev/null +++ b/thanasoft-back/tests/Feature/EmployeeAgendaApiTest.php @@ -0,0 +1,148 @@ +authenticatedUser = User::factory()->create(); + Sanctum::actingAs($this->authenticatedUser); + } + + public function test_can_get_employee_agenda_filtered_by_month_and_status(): void + { + $client = Client::factory()->create(); + + $employee = Employee::create([ + 'first_name' => 'Jean', + 'last_name' => 'Martin', + 'email' => 'jean.martin@example.test', + 'job_title' => 'Thanatopracteur', + 'active' => true, + ]); + + $otherEmployee = Employee::create([ + 'first_name' => 'Paul', + 'last_name' => 'Durand', + 'email' => 'paul.durand@example.test', + 'job_title' => 'Thanatopracteur', + 'active' => true, + ]); + + $thanatopractitioner = Thanatopractitioner::create([ + 'employee_id' => $employee->id, + 'authorization_number' => 'AUTH-001', + ]); + + $otherThanatopractitioner = Thanatopractitioner::create([ + 'employee_id' => $otherEmployee->id, + 'authorization_number' => 'AUTH-002', + ]); + + $matchingIntervention = Intervention::create([ + 'client_id' => $client->id, + 'type' => 'thanatopraxie', + 'scheduled_at' => '2026-04-15 09:00:00', + 'duration_min' => 90, + 'status' => 'planifie', + 'created_by' => $this->authenticatedUser->id, + ]); + + $sameEmployeeOutsideMonth = Intervention::create([ + 'client_id' => $client->id, + 'type' => 'thanatopraxie', + 'scheduled_at' => '2026-05-03 10:00:00', + 'duration_min' => 60, + 'status' => 'planifie', + 'created_by' => $this->authenticatedUser->id, + ]); + + $sameMonthDifferentStatus = Intervention::create([ + 'client_id' => $client->id, + 'type' => 'thanatopraxie', + 'scheduled_at' => '2026-04-18 11:00:00', + 'duration_min' => 45, + 'status' => 'termine', + 'created_by' => $this->authenticatedUser->id, + ]); + + $otherEmployeeIntervention = Intervention::create([ + 'client_id' => $client->id, + 'type' => 'toilette_mortuaire', + 'scheduled_at' => '2026-04-20 14:00:00', + 'duration_min' => 30, + 'status' => 'planifie', + 'created_by' => $this->authenticatedUser->id, + ]); + + $matchingIntervention->practitioners()->attach($thanatopractitioner->id, [ + 'role' => 'principal', + 'assigned_at' => now(), + ]); + + $sameEmployeeOutsideMonth->practitioners()->attach($thanatopractitioner->id, [ + 'role' => 'assistant', + 'assigned_at' => now(), + ]); + + $sameMonthDifferentStatus->practitioners()->attach($thanatopractitioner->id, [ + 'role' => 'assistant', + 'assigned_at' => now(), + ]); + + $otherEmployeeIntervention->practitioners()->attach($otherThanatopractitioner->id, [ + 'role' => 'principal', + 'assigned_at' => now(), + ]); + + $response = $this->getJson(sprintf( + '/api/employees/%d/agenda?month=2026-04&status=planifie', + $employee->id + )); + + $response->assertStatus(200) + ->assertJsonPath('employee.id', $employee->id) + ->assertJsonPath('employee.thanatopractitioner_id', $thanatopractitioner->id) + ->assertJsonPath('filters.month', '2026-04') + ->assertJsonPath('meta.total', 1) + ->assertJsonPath('meta.status_summary.planifie', 1) + ->assertJsonCount(1, 'data') + ->assertJsonPath('data.0.id', $matchingIntervention->id) + ->assertJsonPath('data.0.status', 'planifie'); + } + + public function test_returns_empty_agenda_when_employee_has_no_thanatopractitioner(): void + { + $employee = Employee::create([ + 'first_name' => 'Luc', + 'last_name' => 'Bernard', + 'email' => 'luc.bernard@example.test', + 'job_title' => 'Assistant', + 'active' => true, + ]); + + $response = $this->getJson('/api/employees/' . $employee->id . '/agenda'); + + $response->assertStatus(200) + ->assertJsonPath('employee.id', $employee->id) + ->assertJsonPath('employee.full_name', 'Luc Bernard') + ->assertJsonPath('meta.total', 0) + ->assertJsonPath('data', []); + } +} \ No newline at end of file diff --git a/thanasoft-back/tests/Feature/StockMoveApiTest.php b/thanasoft-back/tests/Feature/StockMoveApiTest.php new file mode 100644 index 0000000..2f7b252 --- /dev/null +++ b/thanasoft-back/tests/Feature/StockMoveApiTest.php @@ -0,0 +1,65 @@ +create()); + } + + public function test_can_list_stock_moves(): void + { + StockMove::factory()->count(2)->create(); + + $response = $this->getJson('/api/stock-moves'); + + $response->assertStatus(200) + ->assertJsonCount(2, 'data'); + } + + public function test_can_create_stock_move(): void + { + $product = Product::factory()->create(); + $warehouse = Warehouse::factory()->create(); + + $data = [ + 'product_id' => $product->id, + 'to_warehouse_id' => $warehouse->id, + 'qty_base' => 10, + 'move_type' => 'receipt', + 'moved_at' => now()->toDateTimeString(), + ]; + + $response = $this->postJson('/api/stock-moves', $data); + + $response->assertStatus(201) + ->assertJsonPath('data.qty_base', 10); + + $this->assertDatabaseHas('stock_moves', [ + 'product_id' => $product->id, + 'qty_base' => 10 + ]); + } + + public function test_valide_french_messages_for_stock_move(): void + { + $response = $this->postJson('/api/stock-moves', []); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['product_id', 'qty_base', 'move_type']) + ->assertJsonFragment(['product_id' => ['Le produit est requis.']]); + } +} diff --git a/thanasoft-back/tests/Feature/WarehouseApiTest.php b/thanasoft-back/tests/Feature/WarehouseApiTest.php new file mode 100644 index 0000000..db5ba72 --- /dev/null +++ b/thanasoft-back/tests/Feature/WarehouseApiTest.php @@ -0,0 +1,89 @@ +create()); + } + + public function test_can_list_warehouses(): void + { + Warehouse::factory()->count(3)->create(); + + $response = $this->getJson('/api/warehouses'); + + $response->assertStatus(200) + ->assertJsonCount(3, 'data'); + } + + public function test_can_create_warehouse(): void + { + $data = [ + 'name' => 'Main Warehouse', + 'city' => 'Paris', + 'country_code' => 'FR', + ]; + + $response = $this->postJson('/api/warehouses', $data); + + $response->assertStatus(201) + ->assertJsonPath('data.name', 'Main Warehouse'); + + $this->assertDatabaseHas('warehouses', ['name' => 'Main Warehouse']); + } + + public function test_can_show_warehouse(): void + { + $warehouse = Warehouse::factory()->create(); + + $response = $this->getJson('/api/warehouses/' . $warehouse->id); + + $response->assertStatus(200) + ->assertJsonPath('data.id', $warehouse->id); + } + + public function test_can_update_warehouse(): void + { + $warehouse = Warehouse::factory()->create(['name' => 'Old Name']); + + $response = $this->putJson('/api/warehouses/' . $warehouse->id, [ + 'name' => 'New Name' + ]); + + $response->assertStatus(200) + ->assertJsonPath('data.name', 'New Name'); + + $this->assertDatabaseHas('warehouses', ['id' => $warehouse->id, 'name' => 'New Name']); + } + + public function test_can_delete_warehouse(): void + { + $warehouse = Warehouse::factory()->create(); + + $response = $this->deleteJson('/api/warehouses/' . $warehouse->id); + + $response->assertStatus(200); + $this->assertDatabaseMissing('warehouses', ['id' => $warehouse->id]); + } + + public function test_valide_french_messages(): void + { + $response = $this->postJson('/api/warehouses', []); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['name']) + ->assertJsonFragment(['name' => ['Le nom de l\'entrepôt est requis.']]); + } +} diff --git a/thanasoft-front/.env b/thanasoft-front/.env index 18d6ec6..f6c84d6 100644 --- a/thanasoft-front/.env +++ b/thanasoft-front/.env @@ -1,3 +1,4 @@ # API base URL for axios (used by src/services/http.ts) # For Laravel Sanctum on local dev (default Laravel port): VUE_APP_API_BASE_URL=http://localhost:8000 +NODE_ENV=production diff --git a/thanasoft-front/.eslintrc.js b/thanasoft-front/.eslintrc.js index 1a4cb3a..1d7d151 100644 --- a/thanasoft-front/.eslintrc.js +++ b/thanasoft-front/.eslintrc.js @@ -24,7 +24,6 @@ module.exports = { // Relax console/debugger in development "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", - }, ignorePatterns: [ @@ -43,5 +42,4 @@ module.exports = { }, }, ], - }; diff --git a/thanasoft-front/CLIENT_CREATION_FLOW.md b/thanasoft-front/CLIENT_CREATION_FLOW.md new file mode 100644 index 0000000..c640f20 --- /dev/null +++ b/thanasoft-front/CLIENT_CREATION_FLOW.md @@ -0,0 +1,271 @@ +# Client Creation Flow - Complete Implementation + +## Overview +Complete implementation of the client creation feature with: +- ✅ Form validation +- ✅ Error handling (Laravel validation errors) +- ✅ Loading states +- ✅ Success messages +- ✅ Auto-redirect after success +- ✅ Store integration + +## Data Flow + +``` +AddClient.vue (Page) + ↓ (passes props & handles events) +AddClientPresentation.vue (Organism) + ↓ (passes props & emits) +NewClientForm.vue (Form Component) + ↓ (user fills form & clicks submit) + ↑ (emits createClient event) +AddClient.vue (receives event) + ↓ (calls store) +clientStore.createClient() + ↓ (API call) +Laravel ClientController + ↓ (validates with StoreClientRequest) + ↓ (creates client or returns 422 errors) + ↑ (returns success or validation errors) +AddClient.vue (handles response) + ↓ (passes validation errors back to form) +NewClientForm.vue (displays errors under inputs) +``` + +## Components Updated + +### 1. AddClient.vue (Page Component) +**Location:** `src/views/pages/CRM/AddClient.vue` + +**Responsibilities:** +- Fetch client categories on mount +- Handle form submission via `handleCreateClient` +- Call store to create client +- Handle validation errors from API (422 status) +- Show success message +- Redirect to clients list after success + +**Key Code:** +```vue + +``` + +### 2. AddClientPresentation.vue (Organism) +**Location:** `src/components/Organism/CRM/AddClientPresentation.vue` + +**Responsibilities:** +- Pass props to NewClientForm +- Relay events from form to parent + +**Props Added:** +- `loading`: Boolean - loading state from store +- `validationErrors`: Object - validation errors from API +- `success`: Boolean - success state + +### 3. NewClientForm.vue (Form Component) +**Location:** `src/components/molecules/form/NewClientForm.vue` + +**Responsibilities:** +- Display form fields with proper validation styling +- Watch for validation errors from parent +- Display errors below each input +- Show loading spinner on button +- Show success alert +- Emit createClient event with form data +- Reset form on success + +**Key Features:** +```vue +// Watch for validation errors +watch(() => props.validationErrors, (newErrors) => { + fieldErrors.value = { ...newErrors }; +}, { deep: true }); + +// Watch for success +watch(() => props.success, (newSuccess) => { + if (newSuccess) { + resetForm(); + } +}); + +// Submit form +const submitForm = async () => { + fieldErrors.value = {}; + errors.value = []; + emit("createClient", form); +}; +``` + +**Validation Error Display:** +```vue + +
+ {{ fieldErrors.name }} +
+``` + +## Laravel Backend + +### StoreClientRequest Validation Rules + +```php +public function rules(): array +{ + return [ + 'client_category_id' => 'nullable', + 'name' => 'required|string|max:255', + 'vat_number' => 'nullable|string|max:32', + 'siret' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:191', + 'phone' => 'nullable|string|max:50', + 'billing_address_line1' => 'nullable|string|max:255', + 'billing_address_line2' => 'nullable|string|max:255', + 'billing_postal_code' => 'nullable|string|max:20', + 'billing_city' => 'nullable|string|max:191', + 'billing_country_code' => 'nullable|string|size:2', + 'group_id' => 'nullable|exists:client_groups,id', + 'notes' => 'nullable|string', + 'is_active' => 'boolean', + 'default_tva_rate_id' => 'nullable|exists:tva_rates,id', + ]; +} +``` + +## Test Data (Postman) + +```json +{ + "client_category_id": 1, + "name": "SARL TechnoPlus", + "vat_number": "FR98765432109", + "siret": "98765432100019", + "email": "compta@technoplus.fr", + "phone": "+33198765432", + "billing_address_line1": "789 Boulevard Haussmann", + "billing_postal_code": "75009", + "billing_city": "Paris", + "billing_country_code": "FR", + "notes": "Nouveau client entreprise", + "is_active": true +} +``` + +## Error Handling + +### Validation Errors (422) +When Laravel returns validation errors: +```json +{ + "message": "The given data was invalid.", + "errors": { + "name": ["Le nom du client est obligatoire."], + "email": ["L'adresse email doit être valide."] + } +} +``` + +These are automatically displayed below each input field. + +### Server Errors (500) +When server error occurs: +- Alert is shown with error message +- User can retry + +### Network Errors +When network is unavailable: +- Alert is shown with generic error message + +## User Experience Flow + +1. **User fills the form** + - All fields are validated client-side (maxlength, email format) + +2. **User clicks "Créer le client"** + - Button shows loading spinner + - Button is disabled + - Previous errors are cleared + +3. **If validation fails (422)** + - Errors appear below each invalid field in red + - Loading stops + - Button is re-enabled + - User can fix errors and resubmit + +4. **If creation succeeds** + - Success message appears in green + - Form is reset + - After 2 seconds, user is redirected to clients list + +## Required Fields + +Only **name** is required. All other fields are optional. + +- ✅ Name: Required (max 255 chars) +- ⭕ Email: Optional but must be valid email format +- ⭕ VAT Number: Optional (max 32 chars) +- ⭕ SIRET: Optional (max 20 chars) +- ⭕ Phone: Optional (max 50 chars) +- ⭕ Address fields: Optional +- ⭕ Country code: Optional (must be 2 chars if provided) +- ⭕ Notes: Optional +- ✅ Active status: Defaults to true + +## CSS Classes for Validation + +```css +.is-invalid { + border-color: #f5365c !important; +} + +.invalid-feedback { + display: block; + color: #f5365c; + font-size: 0.875rem; + margin-top: 0.25rem; +} +``` + +## Testing Checklist + +- [ ] Submit empty form → "name" required error shows +- [ ] Submit with invalid email → email validation error shows +- [ ] Submit with VAT > 32 chars → VAT length error shows +- [ ] Submit with SIRET > 20 chars → SIRET length error shows +- [ ] Submit valid data → Success message & redirect +- [ ] Check loading spinner appears during submission +- [ ] Check button is disabled during submission +- [ ] Check form resets after success +- [ ] Check redirect happens after 2 seconds + +## Future Enhancements + +- [ ] Add client category dropdown (currently just ID) +- [ ] Add client group dropdown +- [ ] Add TVA rate dropdown +- [ ] Add real-time validation as user types +- [ ] Add confirmation modal before submit +- [ ] Add ability to create contact at same time +- [ ] Add file upload for documents diff --git a/thanasoft-front/CLIENT_DETAIL_NEW.md b/thanasoft-front/CLIENT_DETAIL_NEW.md new file mode 100644 index 0000000..35533a1 --- /dev/null +++ b/thanasoft-front/CLIENT_DETAIL_NEW.md @@ -0,0 +1,259 @@ +# New Client Detail Page - Modern Design + +## Overview +A completely redesigned client detail page with modern UI/UX, inspired by the Settings page structure with tabs, client avatar/logo, and better ergonomics. + +## Features + +### 🎨 **Modern Layout** +- **Left Sidebar**: Sticky client card with navigation +- **Right Content Area**: Tabbed content with clean organization + +### 👤 **Client Profile Card** +- **Avatar/Logo**: + - Shows client initials if no image + - Click edit button to upload logo + - Large, prominent display +- **Quick Stats**: + - Number of contacts + - Active/Inactive status +- **Client Info**: Name and type + +### 📑 **5 Tab Sections** + +#### 1. **Aperçu (Overview)** +Default view with key information: +- Contact info card (email, phone) +- Business info card (SIRET, VAT) +- Address card +- Recent contacts (first 3) +- Edit button to modify client + +#### 2. **Informations (Information)** +Complete client details: +- Name, type +- SIRET, VAT number +- Email, phone +- Commercial + +#### 3. **Contacts** +Full contact list with: +- Contact table with avatars +- Email, phone, position +- Primary contact badge +- "Add Contact" button +- Empty state if no contacts + +#### 4. **Adresse (Address)** +Billing address details: +- Address line 1 & 2 +- Postal code, city, country + +#### 5. **Notes** +Client notes section + +## File Location +``` +src/views/pages/CRM/ClientDetailNew.vue +``` + +## Usage + +### Update Router +Add the new route in your router configuration: + +```javascript +// router/index.js +{ + path: '/clients/:id/detail', + name: 'ClientDetailNew', + component: () => import('@/views/pages/CRM/ClientDetailNew.vue') +} +``` + +### Replace Old ClientDetail +To use this as the main client detail page: + +```javascript +// Change existing route +{ + path: '/clients/:id', + name: 'ClientDetail', + component: () => import('@/views/pages/CRM/ClientDetailNew.vue') // Changed from ClientDetails.vue +} +``` + +## Component Structure + +### Template Sections +1. **Header** - Back button to clients list +2. **Loading State** - Spinner while fetching data +3. **Error State** - Alert for errors +4. **Main Content** + - Left: Client card + navigation + - Right: Tab content + +### Script Features +```javascript +// Reactive data +const activeTab = ref('overview') // Current tab +const clientAvatar = ref(null) // Client logo/avatar +const contacts_client = ref([]) // Client contacts + +// Methods +getInitials(name) // Generate initials from name +formatAddress(client) // Format full address string +triggerFileInput() // Open file selector +handleAvatarUpload() // Handle logo upload +``` + +### Styling Features +- **Sticky sidebar** - Stays visible when scrolling +- **Active tab highlighting** - Gradient background +- **Smooth transitions** - Hover effects +- **Responsive design** - Works on mobile +- **Clean cards** - Soft shadows and borders + +## Design Principles + +### Color Scheme +- **Primary Actions**: Gradient purple-pink (`#7928ca` to `#ff0080`) +- **Success**: Green for active status and badges +- **Info**: Blue for contact info +- **Warning**: Yellow for business info +- **Danger**: Red for inactive status + +### Typography +- **Headers**: Bold, clear hierarchy +- **Body**: `text-sm` for readability +- **Labels**: Uppercase with spacing + +### Icons +Using Font Awesome: +- 📊 `fa-eye` - Overview +- ℹ️ `fa-info-circle` - Information +- 👥 `fa-users` - Contacts +- 📍 `fa-map-marker-alt` - Address +- 📝 `fa-sticky-note` - Notes + +## Avatar/Logo Upload + +### Current Implementation +```javascript +handleAvatarUpload(event) { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + clientAvatar.value = e.target.result; + // TODO: Upload to server + }; + reader.readAsDataURL(file); + } +} +``` + +### TODO: Connect to Backend +To implement server upload: + +```javascript +const handleAvatarUpload = async (event) => { + const file = event.target.files[0]; + if (!file) return; + + const formData = new FormData(); + formData.append('avatar', file); + formData.append('client_id', clientStore.currentClient.id); + + try { + const response = await api.post('/clients/upload-avatar', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }); + + clientAvatar.value = response.data.avatar_url; + // Update store + await clientStore.fetchClient(client_id); + } catch (error) { + console.error('Upload failed:', error); + } +}; +``` + +## Comparison: Old vs New + +### Old Design +- Single page layout +- All info visible at once +- No image/avatar support +- Basic styling +- No tab organization + +### New Design ✨ +- Modern tabbed interface +- Client avatar/logo with upload +- Sticky sidebar navigation +- Card-based organization +- Better mobile responsive +- Visual hierarchy +- Quick stats +- Empty states +- Badge indicators + +## Customization + +### Change Tab Order +```vue + + +``` + +### Add New Tab +1. Add nav item in sidebar +2. Add content section with `v-show` +3. Update `activeTab` ref + +```vue + + + + +
+
+
Documents
+
+
+ +
+
+``` + +## Dependencies +- Vue 3 Composition API +- Vue Router +- Pinia stores (clientStore, contactStore) +- Font Awesome icons +- Bootstrap 5 classes + +## Browser Support +- Chrome, Firefox, Safari (latest) +- Edge (latest) +- Mobile browsers + +--- + +**Status**: ✅ Ready to use +**Created**: October 20, 2025 +**File**: `src/views/pages/CRM/ClientDetailNew.vue` diff --git a/thanasoft-front/CONTACTFORM_CLIENT_SELECTION.md b/thanasoft-front/CONTACTFORM_CLIENT_SELECTION.md new file mode 100644 index 0000000..4ac31bc --- /dev/null +++ b/thanasoft-front/CONTACTFORM_CLIENT_SELECTION.md @@ -0,0 +1,120 @@ +# ContactForm Client Selection - Simplified + +## What Changed + +The client selection has been simplified to **only store the client_id** without modifying any form fields. + +## Previous Behavior ❌ +- Selecting a client pre-filled form fields (first_name, last_name, email, phone, mobile) +- This was confusing because you're creating a new contact, not editing client data + +## New Behavior ✅ +- Selecting a client **only stores the client_id** +- Form fields remain empty for you to fill in the contact's information +- Shows "Client sélectionné" banner with client name and email +- Parent component receives the client_id via `clientSelected` event + +## How It Works + +### 1. Select Client +```vue + +``` + +### 2. Store Client Reference +```javascript +const selectClient = (client) => { + // Store the selected client (to show which client is selected) + selectedContact.value = client; + + // Clear search + searchQuery.value = ""; + showDropdown.value = false; + + // Emit client selected event with client ID + emit("clientSelected", client.id); +}; +``` + +### 3. Display Selected Client +Shows a green banner: +``` +✓ Client sélectionné + [Client Name] • [client@email.com] + Le contact sera lié à ce client + [Changer button] +``` + +### 4. Parent Handles Submission +The parent component should listen for `clientSelected` and add `client_id` to the form data: + +```javascript +const selectedClientId = ref(null); + +const handleClientSelected = (clientId) => { + selectedClientId.value = clientId; +}; + +const handleCreateContact = (formData) => { + // Add client_id to the form data before submitting + const payload = { + ...formData, + client_id: selectedClientId.value + }; + + // Submit to API + await api.post('/contacts', payload); +}; +``` + +## Events Emitted + +- `clientSelected(clientId)` - When a client is selected or cleared + - `clientId`: The ID of the selected client, or `null` if cleared + +- `createContact(formData)` - When the form is submitted + - Parent should add `client_id` to formData before API call + +## Clear Selection + +Clicking "Changer" button: +- Clears the selected client +- Emits `clientSelected(null)` +- Does NOT clear form fields (user can keep typing) + +## Usage Example + +```vue + +``` + +```javascript +const selectedClientId = ref(null); + +const handleClientSelected = (clientId) => { + selectedClientId.value = clientId; +}; + +const handleCreateContact = async (formData) => { + try { + const payload = { + ...formData, + client_id: selectedClientId.value // Add the selected client ID + }; + + await contactApi.create(payload); + } catch (error) { + console.error(error); + } +}; +``` + +## File Modified +- `src/components/molecules/form/ContactForm.vue` diff --git a/thanasoft-front/CONTACTFORM_FIX.md b/thanasoft-front/CONTACTFORM_FIX.md new file mode 100644 index 0000000..971248d --- /dev/null +++ b/thanasoft-front/CONTACTFORM_FIX.md @@ -0,0 +1,95 @@ +# ContactForm Client Dropdown Fix + +## Problem +The client dropdown in ContactForm was showing "Aucun client trouvé" even when the API returned results. The component was trying to iterate over `client` in the template but reference it as `contact` which didn't exist. + +## Issues Found + +### 1. Variable Name Mismatch (Line 51, 57) +```vue + + + + + +``` + +**Why it failed**: The loop variable was `client` but the template tried to access `contact`, which was undefined, causing nothing to render. + +### 2. Missing Method +The component was calling `selectClient()` but only had `selectContact()` method defined. + +### 3. Event Emission Mismatch +The component was emitting `searchClient` in the code but the parent might be listening for `searchContact`. + +## Changes Made + +### 1. Fixed Template Variable References +- Changed `@mousedown="selectContact(contact)"` → `@mousedown="selectClient(client)"` +- Changed `{{ contact.name }}` → `{{ client.name }}` +- Added client email display in dropdown + +### 2. Added `selectClient()` Method +```javascript +const selectClient = (client) => { + selectedContact.value = client; + + // Split name into first/last if needed + if (client.name) { + const nameParts = client.name.split(' '); + if (nameParts.length > 1) { + form.value.first_name = nameParts[0]; + form.value.last_name = nameParts.slice(1).join(' '); + } else { + form.value.first_name = client.name; + } + } + + form.value.email = client.email || ""; + form.value.phone = client.phone || ""; + form.value.mobile = client.mobile || ""; + + searchQuery.value = ""; + showDropdown.value = false; + + emit("clientSelected", client.id); +}; +``` + +### 3. Updated Emits Declaration +Added `searchClient` and `clientSelected` to the emits array: +```javascript +const emit = defineEmits([ + "createContact", + "searchClient", // For searching clients + "clientSelected", // When a client is selected + "contactSelected" // When a contact is selected +]); +``` + +## How It Works Now + +1. User types in search input → emits `searchClient` event +2. Parent component makes API call to `/api/clients/searchBy?name=...` +3. Results are passed back via `searchResults` prop +4. Dropdown shows clients with name and email +5. User clicks a client → `selectClient()` is called +6. Form is pre-filled with client data (name split into first/last, email, phone) +7. Parent receives `clientSelected` event with client ID + +## Testing + +To verify the fix works: + +1. Start typing a client name in the search field +2. The dropdown should now display the matching clients +3. Click on a client +4. The form should pre-fill with the client's information +5. The contact can then be created and linked to that client + +## File Modified +- `src/components/molecules/form/ContactForm.vue` diff --git a/thanasoft-front/CONTACT_TABLE_FIX.md b/thanasoft-front/CONTACT_TABLE_FIX.md new file mode 100644 index 0000000..07af5a0 --- /dev/null +++ b/thanasoft-front/CONTACT_TABLE_FIX.md @@ -0,0 +1,205 @@ +# ContactTable Component Fix + +## Problem +The `` component was not showing in the Contacts.vue page. + +## Root Cause +The **ContactTable.vue** component had an incomplete ` +``` + +**After:** +```vue + +``` + +**Key fixes:** +- ✅ Added ` +``` + +**After:** +```vue + +``` + +**Key fixes:** +- ✅ Removed TypeScript (`lang="ts"`) +- ✅ Consistent quote style +- ✅ Properly imports ContactTable component + +## How ` +``` + +### ✅ Correct - Script setup: +```vue + +``` + +### ❌ Wrong - Missing imports: +```vue + + +``` + +### ✅ Correct - With imports: +```vue + + +``` + +## Benefits of This Approach + +1. **Simpler**: No TypeScript complexity +2. **Cleaner**: Less boilerplate code +3. **Standard**: Follows Vue 3 best practices +4. **Maintainable**: Easy to understand and modify +5. **Performant**: ` +``` + +### Make the data dynamic: +```vue + +``` + +Then loop through contacts in the template with `v-for`. diff --git a/thanasoft-front/CONTACT_TABLE_UPDATE.md b/thanasoft-front/CONTACT_TABLE_UPDATE.md new file mode 100644 index 0000000..4a39409 --- /dev/null +++ b/thanasoft-front/CONTACT_TABLE_UPDATE.md @@ -0,0 +1,181 @@ +# ContactTable Component - Complete Redesign + +## Overview +The ContactTable has been completely rewritten to match the ClientTable structure with improved features, loading states, and action buttons. + +## New Features Added + +### 1. **Loading State with Skeleton Screen** +- Shows animated skeleton rows while data is loading +- Loading spinner in top-right corner +- Smooth pulse animation for better UX + +### 2. **Complete Data Display** +Columns now show: +- **Contact Name** - With avatar and full name +- **Client** - Shows associated client name (or "-" if none) +- **Email** - Contact email address +- **Phone / Mobile** - Both phone numbers with icons +- **Position** - Job title/position +- **Status** - Shows star badge if primary contact +- **Actions** - View, Edit, Delete buttons + +### 3. **Action Buttons** +Three action buttons per contact: +- **View** (Info/Blue) - View contact details +- **Edit** (Warning/Yellow) - Edit contact information +- **Delete** (Danger/Red) - Delete contact + +### 4. **Empty State** +Shows a friendly message when no contacts exist: +- Address book icon +- "Aucun contact trouvé" message +- Helpful text + +### 5. **DataTable Integration** +- Searchable table +- Pagination (5, 10, 15, 20 per page) +- Fixed height scrolling +- Automatic reinitialization on data changes + +### 6. **Event Emissions** +The component now emits three events: +```javascript +emit("view", contactId) // When view button clicked +emit("edit", contactId) // When edit button clicked +emit("delete", contactId) // When delete button clicked +``` + +## Usage + +### Basic Usage +```vue + +``` + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `data` | Array | `[]` | Array of contact objects | +| `loading` | Boolean | `false` | Shows loading skeleton | +| `skeletonRows` | Number | `5` | Number of skeleton rows to show | + +### Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `@view` | `contactId` | Emitted when view button is clicked | +| `@edit` | `contactId` | Emitted when edit button is clicked | +| `@delete` | `contactId` | Emitted when delete button is clicked | + +### Expected Data Structure + +```javascript +const contacts = [ + { + id: 1, + first_name: "John", + last_name: "Doe", + full_name: "John Doe", // Computed in backend + email: "john@example.com", + phone: "+33 1 23 45 67 89", + mobile: "+33 6 12 34 56 78", + position: "Sales Manager", + is_primary: true, + client: { + id: 5, + name: "Acme Corporation" + } + }, + // ... +] +``` + +## Parent Component Example + +```vue + + + +``` + +## Styling Features + +- **Responsive design** - Adapts to mobile screens +- **Skeleton animations** - Shimmer and pulse effects +- **Icon badges** - For status indicators +- **Avatar images** - Random avatars for visual appeal +- **Consistent spacing** - Matches ClientTable design + +## Dependencies + +- `simple-datatables` - For table pagination and search +- `SoftButton` - Custom button component +- `SoftAvatar` - Avatar component +- Font Awesome icons + +## File Modified +- `src/components/molecules/Tables/ContactTable.vue` + +--- + +**Status**: ✅ Complete +**Date**: October 16, 2025 diff --git a/thanasoft-front/FILE_MANAGEMENT_FRONTEND.md b/thanasoft-front/FILE_MANAGEMENT_FRONTEND.md new file mode 100644 index 0000000..ccd9133 --- /dev/null +++ b/thanasoft-front/FILE_MANAGEMENT_FRONTEND.md @@ -0,0 +1,815 @@ +# File Management Frontend System + +## Overview + +Complete frontend file management system with Vue.js 3, TypeScript, and Pinia for state management. This system provides file upload, organization, filtering, and management capabilities. + +## Architecture + +- **FileService**: API communication and data transformation +- **FileStore**: Pinia store for state management +- **TypeScript Interfaces**: Type safety for all data structures + +## Installation + +The file service and store are ready to use. Import them in your components: + +```typescript +import { useFileStore } from "@/stores/fileStore"; +import FileService from "@/services/file"; +``` + +## FileStore Usage + +### Basic Setup + +```typescript +import { useFileStore } from "@/stores/fileStore"; + +const fileStore = useFileStore(); + +// Reactive state +const { + files, + isLoading, + hasError, + getError, + getPagination, + totalSizeFormatted, +} = storeToRefs(fileStore); + +// Actions +const { fetchFiles, uploadFile, deleteFile, searchFiles } = fileStore; +``` + +### Store State Properties + +#### Files Management + +- `files` - Array of all files +- `currentFile` - Currently selected/viewed file +- `selectedFiles` - Array of selected file IDs for bulk operations +- `filters` - Current filtering criteria +- `pagination` - Pagination metadata + +#### Loading States + +- `loading` - General loading state +- `uploadProgress` - Upload progress (0-100%) +- `error` - Error message +- `hasError` - Boolean error state + +#### Statistics + +- `organizedFiles` - Files grouped by category/subcategory +- `storageStats` - Storage usage statistics + +### Store Actions + +#### File Retrieval + +```typescript +// Get all files with pagination and filters +await fileStore.fetchFiles({ + page: 1, + per_page: 15, + search: "document", + category: "devis", + sort_by: "uploaded_at", + sort_direction: "desc", +}); + +// Get specific file by ID +const file = await fileStore.fetchFile(123); + +// Search files +await fileStore.searchFiles("invoice", { page: 1, per_page: 10 }); + +// Get files by category +await fileStore.fetchFilesByCategory("devis", { per_page: 20 }); + +// Get files by client +await fileStore.fetchFilesByClient(456, { per_page: 15 }); +``` + +#### File Upload + +```typescript +// Upload a single file +const fileInput = ref(); + +const handleFileUpload = async (event: Event) => { + const target = event.target as HTMLInputElement; + const file = target.files?.[0]; + + if (file) { + try { + await fileStore.uploadFile({ + file, + category: "devis", + client_id: 123, + subcategory: "annual", + description: "Annual quote document", + tags: ["quote", "annual"], + is_public: false, + }); + + console.log("File uploaded successfully"); + } catch (error) { + console.error("Upload failed:", error); + } + } +}; +``` + +#### File Management + +```typescript +// Update file metadata +await fileStore.updateFile({ + id: 123, + file_name: "updated_filename.pdf", + description: "Updated description", + tags: ["updated", "tag"], + category: "facture", +}); + +// Delete single file +await fileStore.deleteFile(123); + +// Delete multiple files +await fileStore.deleteMultipleFiles([123, 124, 125]); + +// Download file +await fileStore.downloadFile(123); + +// Generate download URL +const downloadUrl = await fileStore.generateDownloadUrl(123); +``` + +#### Filtering and Organization + +```typescript +// Set filters +fileStore.setFilters({ + category: "devis", + client_id: 123, + date_from: "2024-01-01", + date_to: "2024-12-31", + mime_type: "application/pdf", +}); + +// Clear filters +fileStore.clearFilters(); + +// Get organized structure +await fileStore.fetchOrganizedStructure(); + +// Get storage statistics +await fileStore.fetchStorageStatistics(); +``` + +#### Selection Management + +```typescript +// Select/deselect files +fileStore.selectFile(123); +fileStore.deselectFile(123); + +// Select all/none +fileStore.selectAllFiles(); +fileStore.deselectAllFiles(); + +// Get selected files +const selectedFiles = computed(() => fileStore.getSelectedFiles.value); +``` + +### Computed Properties + +```typescript +// Basic getters +const allFiles = computed(() => fileStore.allFiles); +const isLoading = computed(() => fileStore.isLoading); +const hasError = computed(() => fileStore.hasError); +const totalSize = computed(() => fileStore.totalSizeFormatted); + +// Filtered views +const imageFiles = computed(() => fileStore.imageFiles); +const pdfFiles = computed(() => fileStore.pdfFiles); +const recentFiles = computed(() => fileStore.recentFiles); + +// Grouped views +const filesByCategory = computed(() => fileStore.filesByCategory); +const filesByClient = computed(() => fileStore.filesByClient); + +// Pagination +const pagination = computed(() => fileStore.getPagination); +``` + +## FileService Usage + +### Direct Service Methods + +```typescript +import FileService from "@/services/file"; + +// File validation before upload +const validation = FileService.validateFile(file, 10 * 1024 * 1024); // 10MB +if (!validation.valid) { + console.error(validation.error); + return; +} + +// Format file size +const sizeFormatted = FileService.formatFileSize(1024000); // "1000.00 KB" + +// Get file icon +const icon = FileService.getFileIcon("application/pdf"); // "📄" + +// Check file type +const isImage = FileService.isImageFile("image/jpeg"); // true +const isPdf = FileService.isPdfFile("application/pdf"); // true + +// Get file extension +const extension = FileService.getFileExtension("document.pdf"); // "pdf" +``` + +## Component Examples + +### File List Component + +```vue + + + +``` + +### File Upload Component + +```vue + + + +``` + +### File Statistics Component + +```vue + + + +``` + +## Best Practices + +### Error Handling + +```typescript +try { + await fileStore.uploadFile(payload); + // Success handling +} catch (error: any) { + // Display user-friendly error message + const message = error.response?.data?.message || error.message; + // Show in toast/notification +} +``` + +### File Validation + +```typescript +// Always validate files before upload +const validation = FileService.validateFile(file); +if (!validation.valid) { + showError(validation.error); + return; +} +``` + +### Progress Tracking + +```vue + +``` + +### Bulk Operations + +```typescript +// Delete multiple files +const deleteSelected = async () => { + if (fileStore.selectedFiles.length === 0) return; + + if (confirm(`Supprimer ${fileStore.selectedFiles.length} fichiers ?`)) { + await fileStore.deleteMultipleFiles(fileStore.selectedFiles); + fileStore.deselectAllFiles(); + } +}; +``` + +### Performance Optimization + +```typescript +// Use computed properties for filtered views +const pdfFiles = computed(() => fileStore.pdfFiles); +const imageFiles = computed(() => fileStore.imageFiles); + +// Cache expensive operations +const fileStats = computed(() => { + return { + totalSize: fileStore.totalSizeFormatted, + fileCount: fileStore.files.length, + }; +}); +``` + +## Integration with Backend + +The frontend system integrates with the Laravel backend API through the FileService. All API endpoints are mapped: + +- `GET /api/files` → `FileService.getAllFiles()` +- `POST /api/files` → `FileService.uploadFile()` +- `GET /api/files/{id}` → `FileService.getFile()` +- `PUT /api/files/{id}` → `FileService.updateFile()` +- `DELETE /api/files/{id}` → `FileService.deleteFile()` +- And specialized endpoints for categories, clients, statistics, etc. + +This ensures full compatibility with the backend file management system while providing a rich, type-safe frontend experience. diff --git a/thanasoft-front/NOTIFICATIONS.md b/thanasoft-front/NOTIFICATIONS.md new file mode 100644 index 0000000..e01abe0 --- /dev/null +++ b/thanasoft-front/NOTIFICATIONS.md @@ -0,0 +1,220 @@ +# Système de Notifications + +Un système de notifications toast moderne et réactif pour afficher des messages en haut à droite de l'écran. + +## 📁 Fichiers créés + +- `src/stores/notification.ts` - Store Pinia pour gérer l'état des notifications +- `src/components/NotificationContainer.vue` - Composant conteneur qui affiche les notifications +- `src/composables/useNotification.ts` - Composable pour faciliter l'utilisation +- `src/examples/NotificationExamples.vue` - Exemples d'utilisation + +## 🚀 Utilisation + +### 1. Import dans un composant + +```javascript +import { useNotification } from "@/composables/useNotification"; + +export default { + setup() { + const notification = useNotification(); + + // Votre code... + } +} +``` + +### 2. Notifications CRUD (les plus utilisées) + +```javascript +// Création +notification.created("Le client"); +// Affiche: "Créé avec succès - Le client a été créé avec succès." + +// Mise à jour +notification.updated("La catégorie"); +// Affiche: "Modifié avec succès - La catégorie a été modifié avec succès." + +// Suppression +notification.deleted("Le produit"); +// Affiche: "Supprimé avec succès - Le produit a été supprimé avec succès." +``` + +### 3. Notifications personnalisées + +```javascript +// Succès +notification.success("Titre", "Message de succès"); + +// Erreur +notification.error("Erreur", "Message d'erreur"); + +// Avertissement +notification.warning("Attention", "Message d'avertissement"); + +// Information +notification.info("Info", "Message d'information"); +``` + +### 4. Durée personnalisée + +```javascript +// Par défaut, les notifications durent 5 secondes (5000ms) +// Vous pouvez changer cela : + +notification.created("Le client", 3000); // 3 secondes +notification.success("Titre", "Message", 10000); // 10 secondes +``` + +## 💡 Exemples d'utilisation réels + +### Dans un formulaire de création + +```javascript +const createClient = async () => { + try { + const response = await clientService.create(formData); + notification.created("Le client"); + router.push("/clients"); + } catch (error) { + notification.error("Erreur", "Impossible de créer le client"); + } +}; +``` + +### Dans un formulaire de mise à jour + +```javascript +const updateCategory = async () => { + try { + await categoryService.update(id, formData); + notification.updated("La catégorie"); + } catch (error) { + notification.error("Erreur", error.message); + } +}; +``` + +### Dans une action de suppression + +```javascript +const deleteProduct = async (id) => { + try { + await productService.delete(id); + notification.deleted("Le produit"); + await fetchProducts(); // Recharger la liste + } catch (error) { + notification.error("Erreur", "Impossible de supprimer le produit"); + } +}; +``` + +### Avec SweetAlert2 pour confirmation + +```javascript +const confirmDelete = (id) => { + this.$swal({ + title: "Êtes-vous sûr ?", + text: "Cette action est irréversible !", + icon: "warning", + showCancelButton: true, + confirmButtonText: "Oui, supprimer", + cancelButtonText: "Annuler", + }).then((result) => { + if (result.isConfirmed) { + deleteClient(id); + } + }); +}; + +const deleteClient = async (id) => { + try { + await clientService.delete(id); + notification.deleted("Le client"); + await fetchClients(); + } catch (error) { + notification.error("Erreur", "Impossible de supprimer le client"); + } +}; +``` + +## 🎨 Types de notifications + +| Type | Couleur | Icône | Usage | +|------|---------|-------|-------| +| `success` | Vert | ✓ | Opérations réussies | +| `error` | Rouge | ✗ | Erreurs | +| `warning` | Orange | ⚠ | Avertissements | +| `info` | Bleu | ℹ | Informations | + +## ⚙️ Configuration + +### Position + +Les notifications apparaissent en haut à droite par défaut. Pour changer la position, modifiez le CSS dans `NotificationContainer.vue` : + +```css +.notification-container { + position: fixed; + top: 20px; /* Changer pour bottom: 20px; pour en bas */ + right: 20px; /* Changer pour left: 20px; pour à gauche */ + z-index: 9999; +} +``` + +### Durée par défaut + +Pour changer la durée par défaut (5 secondes), modifiez dans `notification.ts` : + +```typescript +const duration = notification.duration || 5000; // Changer 5000 en valeur désirée +``` + +## 📱 Responsive + +Le système est entièrement responsive. Sur mobile (< 768px), les notifications prennent toute la largeur de l'écran avec une marge de 10px. + +## 🎭 Animations + +Les notifications glissent depuis la droite à l'apparition et disparaissent avec une animation fluide. + +## 🧪 Tester le système + +1. Importez la page d'exemples dans votre router +2. Naviguez vers `/notification-examples` +3. Testez tous les types de notifications + +```javascript +// Dans votre fichier router +{ + path: "/notification-examples", + name: "NotificationExamples", + component: () => import("@/examples/NotificationExamples.vue"), +} +``` + +## 🛠️ Utilisation directe du store (avancé) + +Si vous avez besoin d'un contrôle plus fin : + +```javascript +import { useNotificationStore } from "@/stores/notification"; + +const notificationStore = useNotificationStore(); + +notificationStore.addNotification({ + type: "success", + title: "Titre personnalisé", + message: "Message personnalisé", + duration: 8000 +}); +``` + +## 📝 Notes + +- Les notifications se ferment automatiquement après la durée spécifiée +- Plusieurs notifications peuvent être affichées simultanément +- Chaque notification peut être fermée manuellement en cliquant sur le X +- Les notifications sont gérées globalement via Pinia +- Le composant `NotificationContainer` est déjà intégré dans `App.vue` diff --git a/thanasoft-front/NOTIFICATION_QUICK_START.md b/thanasoft-front/NOTIFICATION_QUICK_START.md new file mode 100644 index 0000000..fffff49 --- /dev/null +++ b/thanasoft-front/NOTIFICATION_QUICK_START.md @@ -0,0 +1,164 @@ +# 🚀 Guide de Démarrage Rapide - Notifications + +## Installation terminée ✅ + +Le système de notifications est déjà intégré dans votre application. Vous pouvez l'utiliser immédiatement ! + +## Utilisation en 3 étapes simples + +### Étape 1 : Importer le composable + +Dans votre composant Vue, ajoutez l'import : + +```javascript +import { useNotification } from "@/composables/useNotification"; +``` + +### Étape 2 : Initialiser dans setup() + +```javascript +export default { + setup() { + const notification = useNotification(); + + return { + notification + }; + } +} +``` + +### Étape 3 : Utiliser dans vos fonctions + +```javascript +// Pour une création +notification.created("Le client"); + +// Pour une mise à jour +notification.updated("La catégorie"); + +// Pour une suppression +notification.deleted("Le produit"); +``` + +## 📋 Exemple complet pour un formulaire de client + +```javascript + +``` + +## 🔄 Pour une mise à jour + +```javascript +const updateClient = async (id, formData) => { + try { + await ClientService.update(id, formData); + notification.updated("Le client"); + } catch (error) { + notification.error("Erreur", error.message); + } +}; +``` + +## ❌ Pour une suppression + +```javascript +const deleteClient = async (id) => { + try { + await ClientService.delete(id); + notification.deleted("Le client"); + await fetchClients(); // Recharger la liste + } catch (error) { + notification.error("Erreur", "Impossible de supprimer le client"); + } +}; +``` + +## 🎨 Autres types de notifications + +```javascript +// Succès personnalisé +notification.success("Bravo !", "L'opération a réussi"); + +// Erreur personnalisée +notification.error("Oups !", "Une erreur s'est produite"); + +// Avertissement +notification.warning("Attention", "Vérifiez vos données"); + +// Information +notification.info("Info", "Nouvelle fonctionnalité disponible"); +``` + +## ⏱️ Changer la durée d'affichage + +Par défaut : 5 secondes. Pour changer : + +```javascript +notification.created("Le client", 3000); // 3 secondes +notification.success("Titre", "Message", 10000); // 10 secondes +``` + +## 📍 Où apparaissent les notifications ? + +- **Position** : En haut à droite de l'écran +- **Mobile** : Pleine largeur en haut +- **Animation** : Glisse depuis la droite +- **Fermeture** : Automatique après 5s ou clic sur X + +## 🧪 Tester le système + +Vous pouvez tester toutes les notifications sur la page d'exemples. +Voir le fichier `NOTIFICATIONS.md` pour plus de détails. + +## 💡 Astuce + +Pour un code plus propre, vous pouvez utiliser le composable directement dans vos méthodes : + +```javascript +const { created, updated, deleted, error, success } = useNotification(); + +// Puis simplement : +created("Le client"); +updated("La catégorie"); +deleted("Le produit"); +``` + +## ❓ Questions fréquentes + +**Q: Les notifications fonctionnent-elles sur toutes les pages ?** +R: Oui, elles sont globales et fonctionnent partout. + +**Q: Puis-je avoir plusieurs notifications en même temps ?** +R: Oui, elles s'empilent automatiquement. + +**Q: Comment changer la position des notifications ?** +R: Voir le fichier `NOTIFICATIONS.md` pour la configuration avancée. + +--- + +📚 **Pour plus d'informations**, consultez `NOTIFICATIONS.md` diff --git a/thanasoft-front/PAYLOAD_CLEANING.md b/thanasoft-front/PAYLOAD_CLEANING.md new file mode 100644 index 0000000..99ea53a --- /dev/null +++ b/thanasoft-front/PAYLOAD_CLEANING.md @@ -0,0 +1,286 @@ +# Payload Cleaning - Form Data Transformation + +## Problem +The form was sending empty strings (`""`) instead of `null` for empty fields, and checkbox was sending `0` instead of `true/false`. + +## Solution +Added data cleaning logic before submitting the form. + +## Changes Made + +### 1. Clean Empty Strings to Null +**Before:** +```json +{ + "name": "", + "email": "", + "phone": "", + "vat_number": "" +} +``` + +**After:** +```json +{ + "name": null, + "email": null, + "phone": null, + "vat_number": null +} +``` + +### 2. Ensure Boolean for is_active +**Before:** +```json +{ + "is_active": 0 // or "" or undefined +} +``` + +**After:** +```json +{ + "is_active": true // or false +} +``` + +### 3. Remove Empty Type Field +The `type` field is not used by the backend, so we remove it if empty. + +## Implementation + +```javascript +const submitForm = async () => { + // Clear errors before submitting + fieldErrors.value = {}; + errors.value = []; + + // Clean up form data: convert empty strings to null + const cleanedForm = {}; + for (const [key, value] of Object.entries(form)) { + if (value === '' || value === null || value === undefined) { + cleanedForm[key] = null; + } else { + cleanedForm[key] = value; + } + } + + // Ensure is_active is boolean + cleanedForm.is_active = Boolean(form.is_active); + + // Remove type field if it's empty (not needed for backend) + if (!cleanedForm.type) { + delete cleanedForm.type; + } + + // Emit the cleaned form data to parent + emit("createClient", cleanedForm); +}; +``` + +## Example Payloads + +### Empty Form (Only Required Fields) +**Before cleaning:** +```json +{ + "client_category_id": null, + "type": "", + "name": "", + "vat_number": "", + "siret": "", + "email": "", + "phone": "", + "billing_address_line1": "", + "billing_address_line2": "", + "billing_postal_code": "", + "billing_city": "", + "billing_country_code": "", + "group_id": null, + "notes": "", + "is_active": 0, + "default_tva_rate_id": null +} +``` + +**After cleaning:** +```json +{ + "client_category_id": null, + "name": null, + "vat_number": null, + "siret": null, + "email": null, + "phone": null, + "billing_address_line1": null, + "billing_address_line2": null, + "billing_postal_code": null, + "billing_city": null, + "billing_country_code": null, + "group_id": null, + "notes": null, + "is_active": true, + "default_tva_rate_id": null +} +``` + +### Filled Form +**Before cleaning:** +```json +{ + "client_category_id": 4, + "type": "", + "name": "SARL TechnoPlus", + "vat_number": "FR98765432109", + "siret": "98765432100019", + "email": "compta@technoplus.fr", + "phone": "+33198765432", + "billing_address_line1": "789 Boulevard Haussmann", + "billing_address_line2": "", + "billing_postal_code": "75009", + "billing_city": "Paris", + "billing_country_code": "FR", + "group_id": null, + "notes": "Nouveau client entreprise", + "is_active": 1, + "default_tva_rate_id": null +} +``` + +**After cleaning:** +```json +{ + "client_category_id": 4, + "name": "SARL TechnoPlus", + "vat_number": "FR98765432109", + "siret": "98765432100019", + "email": "compta@technoplus.fr", + "phone": "+33198765432", + "billing_address_line1": "789 Boulevard Haussmann", + "billing_address_line2": null, + "billing_postal_code": "75009", + "billing_city": "Paris", + "billing_country_code": "FR", + "group_id": null, + "notes": "Nouveau client entreprise", + "is_active": true, + "default_tva_rate_id": null +} +``` + +## Benefits + +### 1. Cleaner Database +- `null` values instead of empty strings +- Easier to query for "no value" vs "empty string" + +### 2. Laravel Validation Works Better +Laravel handles `null` better than `""` for: +- Optional fields +- Email validation (null is ok, "" might fail) +- Exists validation (null is ok, "" will try to find empty key) + +### 3. Boolean Logic Works Correctly +```php +// In Laravel +if ($request->is_active) { + // This now works correctly +} +``` + +### 4. Consistent Data Types +- Strings stay strings +- Nulls stay nulls +- Booleans stay booleans +- Numbers stay numbers + +## Checkbox Behavior + +### Initial State +```vue + +``` + +```javascript +const form = reactive({ + is_active: true, // ✅ Starts checked +}); +``` + +### When User Unchecks +- `form.is_active` becomes `false` +- Payload sends: `"is_active": false` + +### When User Checks +- `form.is_active` becomes `true` +- Payload sends: `"is_active": true` + +## Testing + +### Test 1: Submit Empty Form +Expected validation error: +```json +{ + "errors": { + "name": ["Le nom du client est obligatoire."] + } +} +``` + +### Test 2: Submit with Only Name +Payload sent: +```json +{ + "client_category_id": null, + "name": "Test Client", + "vat_number": null, + "siret": null, + "email": null, + "phone": null, + "billing_address_line1": null, + "billing_address_line2": null, + "billing_postal_code": null, + "billing_city": null, + "billing_country_code": null, + "group_id": null, + "notes": null, + "is_active": true, + "default_tva_rate_id": null +} +``` + +Backend should accept this and create client with only name and is_active set. + +### Test 3: Submit with Inactive Client +Uncheck "Client actif" checkbox, then submit. + +Payload sent: +```json +{ + "name": "Inactive Client", + "is_active": false, + // ... other fields +} +``` + +## Why Not Clean on Backend? + +We clean on frontend because: +1. **Better UX**: Smaller payload size +2. **Clearer Intent**: `null` explicitly means "no value" +3. **Type Safety**: Ensures correct data types before sending +4. **Validation**: Easier to validate before API call +5. **Debugging**: Easier to see what data is being sent + +## Laravel Backend Handles Both + +The Laravel backend will accept: +- `"field": null` ✅ +- `"field": ""` ✅ (but converts to null for nullable fields) +- `"field": "value"` ✅ + +But we send `null` for consistency and clarity. diff --git a/thanasoft-front/README_NOTIFICATIONS.txt b/thanasoft-front/README_NOTIFICATIONS.txt new file mode 100644 index 0000000..31e7e26 --- /dev/null +++ b/thanasoft-front/README_NOTIFICATIONS.txt @@ -0,0 +1,89 @@ +╔══════════════════════════════════════════════════════════════════════╗ +║ SYSTÈME DE NOTIFICATIONS ║ +║ Installation Complète ✅ ║ +╚══════════════════════════════════════════════════════════════════════╝ + +📁 FICHIERS CRÉÉS: + ├── src/stores/notification.ts + │ └── Store Pinia pour gérer les notifications + │ + ├── src/components/NotificationContainer.vue + │ └── Conteneur qui affiche les notifications en haut + │ + ├── src/composables/useNotification.ts + │ └── Hook pratique pour utiliser les notifications + │ + ├── src/examples/NotificationExamples.vue + │ └── Page de démonstration + │ + ├── NOTIFICATIONS.md + │ └── Documentation complète + │ + └── NOTIFICATION_QUICK_START.md + └── Guide de démarrage rapide + +🔧 INTÉGRATION: + ✅ NotificationContainer ajouté dans App.vue + ✅ Prêt à l'emploi dans tous vos composants + ✅ Responsive mobile & desktop + ✅ Animations fluides + +🚀 UTILISATION RAPIDE: + + 1. Importer dans votre composant: + import { useNotification } from "@/composables/useNotification"; + + 2. Initialiser: + const notification = useNotification(); + + 3. Utiliser: + notification.created("Le client"); + notification.updated("La catégorie"); + notification.deleted("Le produit"); + +📋 EXEMPLES: + + ▸ Création réussie: + notification.created("Le client"); + → Affiche: "Créé avec succès - Le client a été créé avec succès." + + ▸ Mise à jour: + notification.updated("La commande"); + → Affiche: "Modifié avec succès - La commande a été modifié avec succès." + + ▸ Suppression: + notification.deleted("Le produit"); + → Affiche: "Supprimé avec succès - Le produit a été supprimé avec succès." + + ▸ Erreur personnalisée: + notification.error("Erreur", "Impossible de se connecter au serveur"); + + ▸ Succès personnalisé: + notification.success("Parfait !", "L'opération a réussi"); + +🎨 TYPES DISPONIBLES: + ✓ success (vert) - Opérations réussies + ✗ error (rouge) - Erreurs + ⚠ warning (orange)- Avertissements + ℹ info (bleu) - Informations + +📍 POSITION: + • Desktop: En haut à droite + • Mobile: Pleine largeur en haut + • Z-index: 9999 (toujours visible) + +⏱️ DURÉE: + • Par défaut: 5 secondes + • Personnalisable: notification.created("Client", 3000) + • Fermeture manuelle: Clic sur ✕ + +📚 DOCUMENTATION: + → NOTIFICATION_QUICK_START.md : Guide de démarrage (recommandé) + → NOTIFICATIONS.md : Documentation complète + +🧪 TESTER: + Créez une route vers NotificationExamples.vue pour tester! + +═══════════════════════════════════════════════════════════════════════ + +Vous êtes prêt à utiliser les notifications ! 🎉 diff --git a/thanasoft-front/lint_output.txt b/thanasoft-front/lint_output.txt new file mode 100644 index 0000000..37e8e1e --- /dev/null +++ b/thanasoft-front/lint_output.txt @@ -0,0 +1,2726 @@ + +> vue-soft-ui-dashboard-pro@3.0.0 lint +> vue-cli-service lint + +[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D` +warning: Prop 'msg' requires default value to be set (vue/require-default-prop) at src/components/HelloWorld.vue:87:5: + 85 | name: "HelloWorld", + 86 | props: { +> 87 | msg: String, + | ^ + 88 | }, + 89 | }); + 90 | + + +warning: Unexpected console statement (no-console) at src/components/Organism/Agenda/InterventionMultiStepModal.vue:758:5: + 756 | productSearchResults.value = products; + 757 | } catch (e) { +> 758 | console.error("Failed to load products", e); + | ^ + 759 | } + 760 | }); + 761 | + + +warning: Unexpected console statement (no-console) at src/components/Organism/Agenda/InterventionMultiStepModal.vue:786:7: + 784 | ); + 785 | } catch (e) { +> 786 | console.error(e); + | ^ + 787 | } + 788 | }, 300); + 789 | }; + + +warning: Unexpected console statement (no-console) at src/components/Organism/Agenda/InterventionMultiStepModal.vue:827:7: + 825 | ); + 826 | } catch (e) { +> 827 | console.error(e); + | ^ + 828 | } + 829 | }, 300); + 830 | }; + + +warning: Unexpected console statement (no-console) at src/components/Organism/Agenda/InterventionMultiStepModal.vue:845:5: + 843 | recentClients.value = clients; + 844 | } catch (e) { +> 845 | console.error("Failed to load recent clients", e); + | ^ + 846 | recentClients.value = []; + 847 | } + 848 | }; + + +warning: Unexpected console statement (no-console) at src/components/Organism/Agenda/InterventionMultiStepModal.vue:893:7: + 891 | productSearchResults.value = products; + 892 | } catch (e) { +> 893 | console.error(e); + | ^ + 894 | productSearchResults.value = []; + 895 | } + 896 | }, 250); + + +warning: Unexpected console statement (no-console) at src/components/Organism/Agenda/InterventionMultiStepModal.vue:947:7: + 945 | locationSearchResults.value = response.data; + 946 | } catch (e) { +> 947 | console.error(e); + | ^ + 948 | } + 949 | }, 300); + 950 | }; + + +warning: Unexpected console statement (no-console) at src/components/Organism/Agenda/InterventionMultiStepModal.vue:1119:5: + 1117 | emit("submit", formData); + 1118 | } catch (e) { +> 1119 | console.error(e); + | ^ + 1120 | globalErrors.value.push("Erreur lors de la préparation du formulaire."); + 1121 | } finally { + 1122 | submitting.value = false; + + +warning: Unexpected console statement (no-console) at src/components/Organism/Agenda/WizardSteps/StepDeceased.vue:280:7: + 278 | showResults.value = true; + 279 | } catch (e) { +> 280 | console.error("Search failed", e); + | ^ + 281 | } finally { + 282 | isSearching.value = false; + 283 | } + + +warning: Unexpected console statement (no-console) at src/components/Organism/Agenda/WizardSteps/StepLocation.vue:286:7: + 284 | showResults.value = true; + 285 | } catch (e) { +> 286 | console.error("Location search failed", e); + | ^ + 287 | } finally { + 288 | isSearching.value = false; + 289 | } + + +warning: Unexpected console statement (no-console) at src/components/Organism/Agenda/WizardSteps/StepProductSelection.vue:185:5: + 183 | searchResults.value = allProducts.value; + 184 | } catch (e) { +> 185 | console.error("Failed to load products", e); + | ^ + 186 | } finally { + 187 | loading.value = false; + 188 | } + + +warning: Unexpected console statement (no-console) at src/components/Organism/Avoir/AvoirDetailPresentation.vue:312:5: + 310 | await avoirStore.fetchAvoir(props.avoirId); + 311 | } catch (err) { +> 312 | console.error("Error fetching avoir:", err); + | ^ + 313 | } + 314 | }); + 315 | + + +warning: Unexpected console statement (no-console) at src/components/Organism/Avoir/AvoirListPresentation.vue:120:5: + 118 | await avoirStore.fetchAvoirs(); + 119 | } catch (err) { +> 120 | console.error("Failed to fetch avoirs:", err); + | ^ + 121 | } + 122 | }); + 123 | + + +warning: Unexpected console statement (no-console) at src/components/Organism/Avoir/NewAvoirPresentation.vue:68:5: + 66 | router.push("/avoirs"); + 67 | } catch (err) { +> 68 | console.error("Error creating avoir:", err); + | ^ + 69 | notificationStore.error("Erreur", "Erreur lors de la création de l'avoir"); + 70 | } + 71 | }; + + +warning: Unexpected console statement (no-console) at src/components/Organism/CRM/EmployeeDetailPresentation.vue:158:7: + 156 | reader.onload = (e) => { + 157 | localAvatar.value = e.target.result; +> 158 | console.log("Upload avatar to server"); + | ^ + 159 | }; + 160 | reader.readAsDataURL(file); + 161 | } + + +warning: Unexpected console statement (no-console) at src/components/Organism/CRM/FournisseurDetailPresentation.vue:197:7: + 195 | localAvatar.value = e.target.result; + 196 | // TODO: Upload to server +> 197 | console.log("Upload avatar to server"); + | ^ + 198 | }; + 199 | reader.readAsDataURL(file); + 200 | } + + +warning: Unexpected console statement (no-console) at src/components/Organism/CRM/contact/AddContactPresentation.vue:42:5: + 40 | () => props.searchResults, + 41 | (newResult, oldResult) => { +> 42 | console.log(newResult); + | ^ + 43 | } + 44 | ); + 45 | + + +warning: The "edit-avatar" event has been triggered but not declared on `emits` option (vue/require-explicit-emits) at src/components/Organism/CRM/employee/EmployeeDetailSidebar.vue:3:58: + 1 | + 483 | + 484 | + + +warning: Unexpected console statement (no-console) at src/views/pages/CRM/EmployeeDetails.vue:54:5: + 52 | const updateEmployee = async (data) => { + 53 | if (!employee_id) { +> 54 | console.error("Missing employee id"); + | ^ + 55 | notificationStore.error("Erreur", "ID de l'employé manquant"); + 56 | return; + 57 | } + + +warning: Unexpected console statement (no-console) at src/views/pages/CRM/EmployeeDetails.vue:69:5: + 67 | notificationStore.updated("Employé"); + 68 | } catch (error) { +> 69 | console.error("Error updating employee:", error); + | ^ + 70 | notificationStore.error("Erreur", "Impossible de mettre à jour l'employé"); + 71 | } + 72 | }; + + +warning: Unexpected console statement (no-console) at src/views/pages/CRM/EmployeeDetails.vue:81:5: + 79 | notificationStore.created("Document"); + 80 | } catch (error) { +> 81 | console.error("Error creating practitioner document:", error); + | ^ + 82 | notificationStore.error("Erreur", "Impossible de créer le document"); + 83 | } + 84 | }; + + +warning: Unexpected console statement (no-console) at src/views/pages/CRM/EmployeeDetails.vue:92:5: + 90 | notificationStore.updated("Document"); + 91 | } catch (error) { +> 92 | console.error("Error updating practitioner document:", error); + | ^ + 93 | notificationStore.error("Erreur", "Impossible de modifier le document"); + 94 | } + 95 | }; + + +warning: Unexpected console statement (no-console) at src/views/pages/CRM/EmployeeDetails.vue:103:5: + 101 | notificationStore.deleted("Document"); + 102 | } catch (error) { +> 103 | console.error("Error removing practitioner document:", error); + | ^ + 104 | notificationStore.error("Erreur", "Impossible de supprimer le document"); + 105 | } + 106 | }; + + +warning: Unexpected console statement (no-console) at src/views/pages/Convoys/AddConvoy.vue:24:5: + 22 | router.push({ name: "Liste convois" }); + 23 | } catch (error) { +> 24 | console.error("Error creating convoy:", error); + | ^ + 25 | notificationStore.error("Erreur", "Impossible de créer le convoi"); + 26 | } + 27 | }; + + +warning: Unexpected console statement (no-console) at src/views/pages/Defunts/AddDefunt.vue:40:5: + 38 | }, 2000); + 39 | } catch (error) { +> 40 | console.error("Error creating deceased:", error); + | ^ + 41 | + 42 | // Handle validation errors from Laravel + 43 | if (error.response && error.response.status === 422) { + + +warning: Unexpected console statement (no-console) at src/views/pages/Defunts/DefuntDetails.vue:39:5: + 37 | if (deceased_id) { + 38 | const cc = await deceasedStore.fetchDeceasedById(deceased_id); +> 39 | console.log(cc); + | ^ + 40 | } + 41 | }); + 42 | + + +warning: Unexpected console statement (no-console) at src/views/pages/Defunts/DefuntDetails.vue:45:5: + 43 | const updateDeceased = async (data) => { + 44 | if (!deceased_id) { +> 45 | console.error("Missing deceased id"); + | ^ + 46 | notificationStore.error("Erreur", "ID du défunt manquant"); + 47 | return; + 48 | } + + +warning: Unexpected console statement (no-console) at src/views/pages/Defunts/DefuntDetails.vue:54:5: + 52 | notificationStore.updated("Personne décédée"); + 53 | } catch (error) { +> 54 | console.error("Error updating deceased:", error); + | ^ + 55 | notificationStore.error( + 56 | "Erreur", + 57 | "Impossible de mettre à jour la personne décédée" + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/AddVehicle.vue:37:5: + 35 | }, 1500); + 36 | } catch (error) { +> 37 | console.error("Error creating vehicle:", error); + | ^ + 38 | + 39 | if (error.response && error.response.status === 422) { + 40 | validationErrors.value = error.response.data.errors || {}; + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Employees.vue:68:3: + 66 | + 67 | const confirmDeleteEmployee = (employeeId) => { +> 68 | console.log("confirmDeleteEmployee called with ID:", employeeId); + | ^ + 69 | console.log("Employee ID type:", typeof employeeId); + 70 | console.log("Store employees:", employeeStore.employees); + 71 | console.log("Store employees length:", employeeStore.employees.length); + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Employees.vue:69:3: + 67 | const confirmDeleteEmployee = (employeeId) => { + 68 | console.log("confirmDeleteEmployee called with ID:", employeeId); +> 69 | console.log("Employee ID type:", typeof employeeId); + | ^ + 70 | console.log("Store employees:", employeeStore.employees); + 71 | console.log("Store employees length:", employeeStore.employees.length); + 72 | + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Employees.vue:70:3: + 68 | console.log("confirmDeleteEmployee called with ID:", employeeId); + 69 | console.log("Employee ID type:", typeof employeeId); +> 70 | console.log("Store employees:", employeeStore.employees); + | ^ + 71 | console.log("Store employees length:", employeeStore.employees.length); + 72 | + 73 | const employee = employeeStore.employees.find((emp) => emp.id === employeeId); + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Employees.vue:71:3: + 69 | console.log("Employee ID type:", typeof employeeId); + 70 | console.log("Store employees:", employeeStore.employees); +> 71 | console.log("Store employees length:", employeeStore.employees.length); + | ^ + 72 | + 73 | const employee = employeeStore.employees.find((emp) => emp.id === employeeId); + 74 | console.log("Found employee:", employee); + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Employees.vue:74:3: + 72 | + 73 | const employee = employeeStore.employees.find((emp) => emp.id === employeeId); +> 74 | console.log("Found employee:", employee); + | ^ + 75 | + 76 | if (employee) { + 77 | console.log("Showing confirmation modal"); + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Employees.vue:77:5: + 75 | + 76 | if (employee) { +> 77 | console.log("Showing confirmation modal"); + | ^ + 78 | confirmModal.isVisible = true; + 79 | confirmModal.employeeId = employeeId; + 80 | confirmModal.employeeName = `${employee.first_name} ${employee.last_name}`; + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Employees.vue:83:5: + 81 | confirmModal.message = `Êtes-vous sûr de vouloir supprimer l'employé "${confirmModal.employeeName}" ?`; + 82 | } else { +> 83 | console.log("No employee found - trying to parse as number"); + | ^ + 84 | const numericId = parseInt(employeeId, 10); + 85 | console.log("Trying with numeric ID:", numericId); + 86 | const employeeByNumber = employeeStore.employees.find( + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Employees.vue:85:5: + 83 | console.log("No employee found - trying to parse as number"); + 84 | const numericId = parseInt(employeeId, 10); +> 85 | console.log("Trying with numeric ID:", numericId); + | ^ + 86 | const employeeByNumber = employeeStore.employees.find( + 87 | (emp) => emp.id === numericId + 88 | ); + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Employees.vue:89:5: + 87 | (emp) => emp.id === numericId + 88 | ); +> 89 | console.log("Found by numeric ID:", employeeByNumber); + | ^ + 90 | + 91 | if (employeeByNumber) { + 92 | console.log("Showing confirmation modal with numeric ID"); + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Employees.vue:92:7: + 90 | + 91 | if (employeeByNumber) { +> 92 | console.log("Showing confirmation modal with numeric ID"); + | ^ + 93 | confirmModal.isVisible = true; + 94 | confirmModal.employeeId = numericId; + 95 | confirmModal.employeeName = `${employeeByNumber.first_name} ${employeeByNumber.last_name}`; + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Employees.vue:98:7: + 96 | confirmModal.message = `Êtes-vous sûr de vouloir supprimer l'employé "${confirmModal.employeeName}" ?`; + 97 | } else { +> 98 | console.log( + | ^ + 99 | "Still no employee found - displaying all employee IDs for comparison" + 100 | ); + 101 | employeeStore.employees.forEach((emp, index) => { + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Employees.vue:102:9: + 100 | ); + 101 | employeeStore.employees.forEach((emp, index) => { +> 102 | console.log( + | ^ + 103 | `Employee ${index}: ID=${emp.id} (${typeof emp.id}), Name=${ + 104 | emp.first_name + 105 | } ${emp.last_name}` + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Employees.vue:115:3: + 113 | const employeeId = confirmModal.employeeId; + 114 | const employeeName = confirmModal.employeeName; +> 115 | console.log("Test"); + | ^ + 116 | try { + 117 | confirmModal.isLoading = true; + 118 | await employeeStore.deleteEmployee(employeeId); + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Employees.vue:125:5: + 123 | closeConfirmModal(); + 124 | } catch (error) { +> 125 | console.error("Error deleting employee:", error); + | ^ + 126 | notificationStore.error( + 127 | "Erreur de suppression", + 128 | "Une erreur est survenue lors de la suppression de l'employé." + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Employees.vue:146:3: + 144 | + 145 | const changePage = async (page) => { +> 146 | console.log("changePage called in Employees.vue with page:", page); + | ^ + 147 | try { + 148 | console.log("Fetching employees with page:", page); + 149 | await employeeStore.fetchEmployees({ page, per_page: 10 }); + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Employees.vue:148:5: + 146 | console.log("changePage called in Employees.vue with page:", page); + 147 | try { +> 148 | console.log("Fetching employees with page:", page); + | ^ + 149 | await employeeStore.fetchEmployees({ page, per_page: 10 }); + 150 | } catch (error) { + 151 | console.error("Error changing page:", error); + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Employees.vue:151:5: + 149 | await employeeStore.fetchEmployees({ page, per_page: 10 }); + 150 | } catch (error) { +> 151 | console.error("Error changing page:", error); + | ^ + 152 | notificationStore.error( + 153 | "Erreur de pagination", + 154 | "Une erreur est survenue lors du changement de page." + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/VehicleDetails.vue:39:5: + 37 | notificationStore.updated("Véhicule"); + 38 | } catch (error) { +> 39 | console.error("Error updating vehicle:", error); + | ^ + 40 | notificationStore.error( + 41 | "Erreur", + 42 | "Impossible de mettre à jour le véhicule" + + +warning: Unexpected console statement (no-console) at src/views/pages/Employes/Vehicules.vue:28:5: + 26 | await vehicleStore.deleteVehicle(vehicleId); + 27 | } catch (error) { +> 28 | console.error("Error deleting vehicle:", error); + | ^ + 29 | } + 30 | }; + 31 | + + +warning: Unexpected console statement (no-console) at src/views/pages/Fournisseurs/AddFournisseur.vue:40:5: + 38 | }, 2000); + 39 | } catch (error) { +> 40 | console.error("Error creating fournisseur:", error); + | ^ + 41 | + 42 | // Handle validation errors from Laravel + 43 | if (error.response && error.response.status === 422) { + + +warning: Unexpected console statement (no-console) at src/views/pages/Fournisseurs/FournisseurDetails.vue:56:5: + 54 | const updateFournisseur = async (data) => { + 55 | if (!fournisseur_id) { +> 56 | console.error("Missing fournisseur id"); + | ^ + 57 | notificationStore.error("Erreur", "ID du fournisseur manquant"); + 58 | return; + 59 | } + + +warning: Unexpected console statement (no-console) at src/views/pages/Fournisseurs/FournisseurDetails.vue:65:5: + 63 | notificationStore.updated("Fournisseur"); + 64 | } catch (error) { +> 65 | console.error("Error updating fournisseur:", error); + | ^ + 66 | notificationStore.error( + 67 | "Erreur", + 68 | "Impossible de mettre à jour le fournisseur" + + +warning: Unexpected console statement (no-console) at src/views/pages/Fournisseurs/FournisseurDetails.vue:86:5: + 84 | notificationStore.created("Contact"); + 85 | } catch (error) { +> 86 | console.error("Error creating contact:", error); + | ^ + 87 | notificationStore.error("Erreur", "Impossible de créer le contact"); + 88 | } + 89 | }; + + +warning: Unexpected console statement (no-console) at src/views/pages/Fournisseurs/FournisseurDetails.vue:97:5: + 95 | notificationStore.updated("Contact"); + 96 | } catch (error) { +> 97 | console.error("Error updating contact:", error); + | ^ + 98 | notificationStore.error("Erreur", "Impossible de modifier le contact"); + 99 | } + 100 | }; + + +warning: Unexpected console statement (no-console) at src/views/pages/Fournisseurs/FournisseurDetails.vue:110:5: + 108 | const createNewLocation = async (data) => { + 109 | try { +> 110 | console.log("Create new location:", data); + | ^ + 111 | // TODO: Implement with location store when ready + 112 | notificationStore.created("Localisation"); + 113 | } catch (error) { + + +warning: Unexpected console statement (no-console) at src/views/pages/Fournisseurs/FournisseurDetails.vue:120:5: + 118 | const modifyLocation = async (location) => { + 119 | try { +> 120 | console.log("Modify location:", location); + | ^ + 121 | // TODO: Implement with location store when ready + 122 | notificationStore.updated("Localisation"); + 123 | } catch (error) { + + +warning: Unexpected console statement (no-console) at src/views/pages/Fournisseurs/FournisseurDetails.vue:130:5: + 128 | const removeLocation = async (locationId) => { + 129 | try { +> 130 | console.log("Remove location:", locationId); + | ^ + 131 | // TODO: Implement with location store when ready + 132 | notificationStore.deleted("Localisation"); + 133 | } catch (error) { + + +warning: Unexpected console statement (no-console) at src/views/pages/Interventions/AddIntervention.vue:72:5: + 70 | }, 2000); + 71 | } catch (error) { +> 72 | console.error("Error creating intervention:", error); + | ^ + 73 | + 74 | // Handle validation errors from Laravel + 75 | if (error.response && error.response.status === 422) { + + +warning: Unexpected console statement (no-console) at src/views/pages/Interventions/AddIntervention.vue:100:5: + 98 | ]); + 99 | } catch (error) { +> 100 | console.error("Error loading data:", error); + | ^ + 101 | notificationStore.error( + 102 | "Erreur", + 103 | "Impossible de charger les données nécessaires" + + +warning: Unexpected console statement (no-console) at src/views/pages/Interventions/InterventionDetails.vue:63:5: + 61 | intervention.value = result; // Store method returns the intervention directly + 62 | } +> 63 | console.log(intervention.value); + | ^ + 64 | } catch (error) { + 65 | console.error("Error loading intervention:", error); + 66 | notificationStore.error( + + +warning: Unexpected console statement (no-console) at src/views/pages/Interventions/InterventionDetails.vue:65:5: + 63 | console.log(intervention.value); + 64 | } catch (error) { +> 65 | console.error("Error loading intervention:", error); + | ^ + 66 | notificationStore.error( + 67 | "Erreur", + 68 | "Impossible de charger les détails de l'intervention" + + +warning: Unexpected console statement (no-console) at src/views/pages/Interventions/InterventionDetails.vue:80:5: + 78 | }); + 79 | } catch (error) { +> 80 | console.error("Error loading practitioners:", error); + | ^ + 81 | notificationStore.error( + 82 | "Erreur", + 83 | "Impossible de charger la liste des praticiens" + + +warning: Unexpected console statement (no-console) at src/views/pages/Interventions/InterventionDetails.vue:121:5: + 119 | } + 120 | } catch (error) { +> 121 | console.error("Error assigning practitioner:", error); + | ^ + 122 | notificationStore.error("Erreur", "Impossible d'assigner le praticien"); + 123 | } + 124 | }; + + +warning: Unexpected console statement (no-console) at src/views/pages/Interventions/InterventionDetails.vue:143:5: + 141 | notificationStore.deleted(practitionerName || "Praticien", "désassigné"); + 142 | } catch (error) { +> 143 | console.error("Error unassigning practitioner:", error); + | ^ + 144 | notificationStore.error("Erreur", "Impossible de désassigner le praticien"); + 145 | } + 146 | }; + + +warning: Unexpected console statement (no-console) at src/views/pages/Interventions/InterventionDetails.vue:157:5: + 155 | notificationStore.updated("Intervention"); + 156 | } catch (error) { +> 157 | console.error("Error updating intervention:", error); + | ^ + 158 | notificationStore.error( + 159 | "Erreur", + 160 | "Impossible de mettre à jour l'intervention" + + +warning: Unexpected console statement (no-console) at src/views/pages/Interventions/InterventionDetails.vue:167:3: + 165 | // Handle cancel + 166 | const handleCancel = () => { +> 167 | console.log("Édition annulée"); + | ^ + 168 | }; + 169 | + 170 | // Watch for changes in intervention store to update local state + + +warning: Unexpected console statement (no-console) at src/views/pages/Interventions/Interventions.vue:27:5: + 25 | }); + 26 | } catch (error) { +> 27 | console.error("Failed to load interventions:", error); + | ^ + 28 | } + 29 | }; + 30 | + + +warning: Unexpected console statement (no-console) at src/views/pages/Interventions/Interventions.vue:38:3: + 36 | // Logic to export CSV - checking if store has action or just trigger download + 37 | // For now just logging, can be implemented via service +> 38 | console.log("Export CSV triggered"); + | ^ + 39 | // Ideally: window.open('/api/interventions/export?format=csv', '_blank'); + 40 | }; + 41 | + + +warning: Unexpected console statement (no-console) at src/views/pages/Parametrage/ProductCategories.vue:79:5: + 77 | await store.fetchCategories(); + 78 | } catch (error) { +> 79 | console.error(error); + | ^ + 80 | } + 81 | }; + 82 | + + +warning: Unexpected console statement (no-console) at src/views/pages/Planning.vue:223:5: + 221 | await Promise.all([fetchMonth(start, force), fetchMonth(end, force)]); + 222 | } catch (error) { +> 223 | console.error("Error loading planning interventions:", error); + | ^ + 224 | notificationStore.error( + 225 | "Erreur", + 226 | "Impossible de charger les interventions du planning" + + +warning: Unexpected console statement (no-console) at src/views/pages/Planning.vue:270:5: + 268 | notificationStore.created("Intervention"); + 269 | } catch (error) { +> 270 | console.error("Error saving intervention:", error); + | ^ + 271 | const errorMessage = + 272 | error.response?.data?.message || + 273 | error.message || + + +warning: Unexpected console statement (no-console) at src/views/pages/Planning.vue:347:3: + 345 | + 346 | const handleCellClick = (info) => { +> 347 | console.log("Cell clicked", info); + | ^ + 348 | }; + 349 | + 350 | const handleEditIntervention = (intervention) => { + + +warning: Unexpected console statement (no-console) at src/views/pages/Planning.vue:351:3: + 349 | + 350 | const handleEditIntervention = (intervention) => { +> 351 | console.log("Edit intervention", intervention); + | ^ + 352 | }; + 353 | + 354 | const handlePrevWeek = () => { + + +warning: Unexpected console statement (no-console) at src/views/pages/Planning.vue:383:5: + 381 | } + 382 | } catch (error) { +> 383 | console.error("Error updating intervention status:", error); + | ^ + 384 | notificationStore.error("Erreur", "Échec de la mise à jour du statut"); + 385 | } + 386 | }; + + +warning: Unexpected console statement (no-console) at src/views/pages/Register.vue:144:5: + 142 | router.push("/dashboards/dashboard-default"); + 143 | } catch (error: any) { +> 144 | console.error("Registration error:", error); + | ^ + 145 | errorMessage.value = + 146 | error.response?.data?.message || + 147 | error.message || + + +warning: Unexpected console statement (no-console) at src/views/pages/Stock/AddProduct.vue:36:5: + 34 | const handleCreateProduct = async (form) => { + 35 | try { +> 36 | console.log(form); + | ^ + 37 | // Clear previous errors + 38 | validationErrors.value = {}; + 39 | showSuccess.value = false; + + +warning: Unexpected console statement (no-console) at src/views/pages/Stock/AddProduct.vue:53:5: + 51 | }, 2000); + 52 | } catch (error) { +> 53 | console.error("Error creating product:", error); + | ^ + 54 | + 55 | // Handle validation errors from Laravel + 56 | if (error.response && error.response.status === 422) { + + +warning: Unexpected console statement (no-console) at src/views/pages/Stock/AddProductCategory.vue:36:5: + 34 | const handleCreateCategory = async (form) => { + 35 | try { +> 36 | console.log(form); + | ^ + 37 | // Clear previous errors + 38 | validationErrors.value = {}; + 39 | showSuccess.value = false; + + +warning: Unexpected console statement (no-console) at src/views/pages/Stock/AddProductCategory.vue:53:5: + 51 | }, 2000); + 52 | } catch (error) { +> 53 | console.error("Error creating product category:", error); + | ^ + 54 | + 55 | // Handle validation errors from Laravel + 56 | if (error.response && error.response.status === 422) { + + +warning: Unexpected console statement (no-console) at src/views/pages/Stock/EditProductCategory.vue:48:5: + 46 | await productCategoryStore.fetchRootProductCategories(); + 47 | } catch (error) { +> 48 | console.error("Error loading product category:", error); + | ^ + 49 | notificationStore.error("Erreur", "Impossible de charger la catégorie"); + 50 | router.push({ name: "Gestion catégories de produits" }); + 51 | } + + +warning: Unexpected console statement (no-console) at src/views/pages/Stock/EditProductCategory.vue:56:5: + 54 | const handleUpdateCategory = async (form) => { + 55 | try { +> 56 | console.log(form); + | ^ + 57 | // Clear previous errors + 58 | validationErrors.value = {}; + 59 | showSuccess.value = false; + + +warning: Unexpected console statement (no-console) at src/views/pages/Stock/EditProductCategory.vue:79:5: + 77 | }, 2000); + 78 | } catch (error) { +> 79 | console.error("Error updating product category:", error); + | ^ + 80 | + 81 | // Handle validation errors from Laravel + 82 | if (error.response && error.response.status === 422) { + + +warning: Unexpected console statement (no-console) at src/views/pages/Stock/EditReception.vue:227:5: + 225 | router.push(`/stock/receptions/${route.params.id}`); + 226 | } catch (error) { +> 227 | console.error("Failed to update goods receipt", error); + | ^ + 228 | } + 229 | }; + 230 | + + +warning: Unexpected console statement (no-console) at src/views/pages/Stock/EditReception.vue:251:5: + 249 | } + 250 | } catch (error) { +> 251 | console.error("Failed to load data", error); + | ^ + 252 | } + 253 | }); + 254 | + + +warning: Unexpected console statement (no-console) at src/views/pages/Stock/ProductCategories.vue:37:7: + 35 | alert("Catégorie supprimée avec succès"); + 36 | } catch (error) { +> 37 | console.error("Erreur lors de la suppression:", error); + | ^ + 38 | alert("Erreur lors de la suppression de la catégorie"); + 39 | } + 40 | } + + +warning: Unexpected console statement (no-console) at src/views/pages/Stock/ProductCategoryDetails.vue:47:5: + 45 | await productCategoryStore.fetchProductCategoryStatistics(); + 46 | } catch (error) { +> 47 | console.error("Error loading product category:", error); + | ^ + 48 | notificationStore.error("Erreur", "Impossible de charger la catégorie"); + 49 | + 50 | // Redirect back to categories list if category not found + + +warning: Unexpected console statement (no-console) at src/views/pages/Stock/ProductCategoryDetails.vue:75:7: + 73 | router.push({ name: "Gestion catégories de produits" }); + 74 | } catch (error) { +> 75 | console.error("Error deleting product category:", error); + | ^ + 76 | + 77 | if (error.response && error.response.status === 422) { + 78 | notificationStore.error( + + +warning: Unexpected console statement (no-console) at src/views/pages/Stock/Products.vue:31:5: + 29 | } catch (error) { + 30 | // Error is already handled in the store +> 31 | console.error("Error deleting product:", error); + | ^ + 32 | } + 33 | }; + 34 | + + +warning: Unexpected console statement (no-console) at src/views/pages/Thanatopractitioners/AddThanatopractitioner.vue:51:5: + 49 | }, 2000); + 50 | } catch (error) { +> 51 | console.error("Error creating thanatopractitioner:", error); + | ^ + 52 | + 53 | // Handle validation errors from Laravel + 54 | if (error.response && error.response.status === 422) { + + +warning: Unexpected console statement (no-console) at src/views/pages/Thanatopractitioners/ThanatopractitionerDetails.vue:48:5: + 46 | ); + 47 | } catch (error) { +> 48 | console.error("Error fetching thanatopractitioner details:", error); + | ^ + 49 | } finally { + 50 | isLoading.value = false; + 51 | } + + +warning: Unexpected console statement (no-console) at src/views/pages/Thanatopractitioners/ThanatopractitionerDetails.vue:62:5: + 60 | ); + 61 | } catch (error) { +> 62 | console.error("Error updating thanatopractitioner:", error); + | ^ + 63 | } + 64 | }; + 65 | + + +warning: Unexpected console statement (no-console) at src/views/pages/Thanatopractitioners/ThanatopractitionerDetails.vue:73:5: + 71 | ); + 72 | } catch (error) { +> 73 | console.error("Error adding document:", error); + | ^ + 74 | } + 75 | }; + 76 | + + +warning: Unexpected console statement (no-console) at src/views/pages/Thanatopractitioners/ThanatopractitionerDetails.vue:86:5: + 84 | ); + 85 | } catch (error) { +> 86 | console.error("Error updating document:", error); + | ^ + 87 | } + 88 | }; + 89 | + + +warning: Unexpected console statement (no-console) at src/views/pages/Thanatopractitioners/Thanatopractitioners.vue:92:5: + 90 | }; + 91 | } catch (error) { +> 92 | console.error("Error fetching thanatopractitioners:", error); + | ^ + 93 | notificationStore.error( + 94 | "Erreur de chargement", + 95 | "Une erreur est survenue lors du chargement des thanatopractitioners." + + +warning: Unexpected console statement (no-console) at src/views/pages/Thanatopractitioners/Thanatopractitioners.vue:143:5: + 141 | }); + 142 | } catch (error) { +> 143 | console.error("Error deleting thanatopractitioner:", error); + | ^ + 144 | notificationStore.error( + 145 | "Erreur de suppression", + 146 | "Une erreur est survenue lors de la suppression du thanatopractitioner." + + +warning: Unexpected console statement (no-console) at src/views/pages/Thanatopractitioners/Thanatopractitioners.vue:171:5: + 169 | }); + 170 | } catch (error) { +> 171 | console.error("Error changing page:", error); + | ^ + 172 | notificationStore.error( + 173 | "Erreur de pagination", + 174 | "Une erreur est survenue lors du changement de page." + + +16 errors and 256 warnings found. diff --git a/thanasoft-front/src/App.vue b/thanasoft-front/src/App.vue index d6f8de6..c6aa10d 100644 --- a/thanasoft-front/src/App.vue +++ b/thanasoft-front/src/App.vue @@ -1,4 +1,7 @@ + + diff --git a/thanasoft-front/src/components/GlobalNotification.vue b/thanasoft-front/src/components/GlobalNotification.vue new file mode 100644 index 0000000..26e7d70 --- /dev/null +++ b/thanasoft-front/src/components/GlobalNotification.vue @@ -0,0 +1,252 @@ + + + + + diff --git a/thanasoft-front/src/components/NotificationContainer.vue b/thanasoft-front/src/components/NotificationContainer.vue new file mode 100644 index 0000000..db6c2f0 --- /dev/null +++ b/thanasoft-front/src/components/NotificationContainer.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Agenda/AgendaPresentation.vue b/thanasoft-front/src/components/Organism/Agenda/AgendaPresentation.vue new file mode 100644 index 0000000..45b66c6 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Agenda/AgendaPresentation.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue b/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue new file mode 100644 index 0000000..ba571b5 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue @@ -0,0 +1,1193 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepClient.vue b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepClient.vue new file mode 100644 index 0000000..738e6d0 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepClient.vue @@ -0,0 +1,234 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepDeceased.vue b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepDeceased.vue new file mode 100644 index 0000000..73150fd --- /dev/null +++ b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepDeceased.vue @@ -0,0 +1,310 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepDocuments.vue b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepDocuments.vue new file mode 100644 index 0000000..ddc9a61 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepDocuments.vue @@ -0,0 +1,152 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepIntervention.vue b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepIntervention.vue new file mode 100644 index 0000000..e9f916f --- /dev/null +++ b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepIntervention.vue @@ -0,0 +1,172 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepLocation.vue b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepLocation.vue new file mode 100644 index 0000000..eaf60e0 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepLocation.vue @@ -0,0 +1,322 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepProductSelection.vue b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepProductSelection.vue new file mode 100644 index 0000000..03227df --- /dev/null +++ b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepProductSelection.vue @@ -0,0 +1,218 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Agenda/WizardSteps/WizardProgress.vue b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/WizardProgress.vue new file mode 100644 index 0000000..7a56762 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/WizardProgress.vue @@ -0,0 +1,41 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Avoir/AvoirDetailPresentation.vue b/thanasoft-front/src/components/Organism/Avoir/AvoirDetailPresentation.vue new file mode 100644 index 0000000..d3f1f64 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Avoir/AvoirDetailPresentation.vue @@ -0,0 +1,560 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Avoir/AvoirListPresentation.vue b/thanasoft-front/src/components/Organism/Avoir/AvoirListPresentation.vue new file mode 100644 index 0000000..36b954a --- /dev/null +++ b/thanasoft-front/src/components/Organism/Avoir/AvoirListPresentation.vue @@ -0,0 +1,123 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Avoir/NewAvoirPresentation.vue b/thanasoft-front/src/components/Organism/Avoir/NewAvoirPresentation.vue new file mode 100644 index 0000000..2841010 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Avoir/NewAvoirPresentation.vue @@ -0,0 +1,72 @@ + + + diff --git a/thanasoft-front/src/components/Organism/CRM/AddClientPresentation.vue b/thanasoft-front/src/components/Organism/CRM/AddClientPresentation.vue new file mode 100644 index 0000000..13e9b69 --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/AddClientPresentation.vue @@ -0,0 +1,44 @@ + + diff --git a/thanasoft-front/src/components/Organism/CRM/AddEmployeePresentation.vue b/thanasoft-front/src/components/Organism/CRM/AddEmployeePresentation.vue new file mode 100644 index 0000000..3d159a6 --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/AddEmployeePresentation.vue @@ -0,0 +1,310 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/CRM/AddFournisseurPresentation.vue b/thanasoft-front/src/components/Organism/CRM/AddFournisseurPresentation.vue new file mode 100644 index 0000000..8a6ee5c --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/AddFournisseurPresentation.vue @@ -0,0 +1,39 @@ + + diff --git a/thanasoft-front/src/components/Organism/CRM/ClientDetailPresentation.vue b/thanasoft-front/src/components/Organism/CRM/ClientDetailPresentation.vue new file mode 100644 index 0000000..9961bd9 --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/ClientDetailPresentation.vue @@ -0,0 +1,213 @@ + + diff --git a/thanasoft-front/src/components/Organism/CRM/ClientPresentation.vue b/thanasoft-front/src/components/Organism/CRM/ClientPresentation.vue new file mode 100644 index 0000000..ed68965 --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/ClientPresentation.vue @@ -0,0 +1,88 @@ + + diff --git a/thanasoft-front/src/components/Organism/CRM/ContactPresentation.vue b/thanasoft-front/src/components/Organism/CRM/ContactPresentation.vue new file mode 100644 index 0000000..1f9b0d6 --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/ContactPresentation.vue @@ -0,0 +1,53 @@ + + diff --git a/thanasoft-front/src/components/Organism/CRM/EmployeeDetailPresentation.vue b/thanasoft-front/src/components/Organism/CRM/EmployeeDetailPresentation.vue new file mode 100644 index 0000000..bcabc12 --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/EmployeeDetailPresentation.vue @@ -0,0 +1,299 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/CRM/FournisseurDetailPresentation.vue b/thanasoft-front/src/components/Organism/CRM/FournisseurDetailPresentation.vue new file mode 100644 index 0000000..a18eab4 --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/FournisseurDetailPresentation.vue @@ -0,0 +1,268 @@ + + + + diff --git a/thanasoft-front/src/components/Organism/CRM/FournisseurPresentation.vue b/thanasoft-front/src/components/Organism/CRM/FournisseurPresentation.vue new file mode 100644 index 0000000..7b21c0b --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/FournisseurPresentation.vue @@ -0,0 +1,108 @@ + + + + diff --git a/thanasoft-front/src/components/Organism/CRM/client/AddChildClientModal.vue b/thanasoft-front/src/components/Organism/CRM/client/AddChildClientModal.vue new file mode 100644 index 0000000..4f8e0ec --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/client/AddChildClientModal.vue @@ -0,0 +1,133 @@ + + + diff --git a/thanasoft-front/src/components/Organism/CRM/client/ClientDetailContent.vue b/thanasoft-front/src/components/Organism/CRM/client/ClientDetailContent.vue new file mode 100644 index 0000000..a8885b5 --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/client/ClientDetailContent.vue @@ -0,0 +1,142 @@ + + + diff --git a/thanasoft-front/src/components/Organism/CRM/client/ClientDetailSidebar.vue b/thanasoft-front/src/components/Organism/CRM/client/ClientDetailSidebar.vue new file mode 100644 index 0000000..baee9c9 --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/client/ClientDetailSidebar.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/CRM/contact/AddContactPresentation.vue b/thanasoft-front/src/components/Organism/CRM/contact/AddContactPresentation.vue new file mode 100644 index 0000000..1b8cacf --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/contact/AddContactPresentation.vue @@ -0,0 +1,45 @@ + + diff --git a/thanasoft-front/src/components/Organism/CRM/employee/EmployeeDetailContent.vue b/thanasoft-front/src/components/Organism/CRM/employee/EmployeeDetailContent.vue new file mode 100644 index 0000000..85891b4 --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/employee/EmployeeDetailContent.vue @@ -0,0 +1,215 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/CRM/employee/EmployeeDetailSidebar.vue b/thanasoft-front/src/components/Organism/CRM/employee/EmployeeDetailSidebar.vue new file mode 100644 index 0000000..e0cf2cf --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/employee/EmployeeDetailSidebar.vue @@ -0,0 +1,400 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/CRM/fournisseur/FournisseurDetailContent.vue b/thanasoft-front/src/components/Organism/CRM/fournisseur/FournisseurDetailContent.vue new file mode 100644 index 0000000..a86199a --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/fournisseur/FournisseurDetailContent.vue @@ -0,0 +1,141 @@ + + + diff --git a/thanasoft-front/src/components/Organism/CRM/fournisseur/FournisseurDetailSidebar.vue b/thanasoft-front/src/components/Organism/CRM/fournisseur/FournisseurDetailSidebar.vue new file mode 100644 index 0000000..708c445 --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/fournisseur/FournisseurDetailSidebar.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/CRM/lieux/ListeLieuxPresentation.vue b/thanasoft-front/src/components/Organism/CRM/lieux/ListeLieuxPresentation.vue new file mode 100644 index 0000000..24149a2 --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/lieux/ListeLieuxPresentation.vue @@ -0,0 +1,75 @@ + + diff --git a/thanasoft-front/src/components/Organism/ClientGroup/ClientGroupDetailPresentation.vue b/thanasoft-front/src/components/Organism/ClientGroup/ClientGroupDetailPresentation.vue new file mode 100644 index 0000000..70af0fa --- /dev/null +++ b/thanasoft-front/src/components/Organism/ClientGroup/ClientGroupDetailPresentation.vue @@ -0,0 +1,158 @@ + + + diff --git a/thanasoft-front/src/components/Organism/ClientGroup/ClientGroupFormPresentation.vue b/thanasoft-front/src/components/Organism/ClientGroup/ClientGroupFormPresentation.vue new file mode 100644 index 0000000..cdd9544 --- /dev/null +++ b/thanasoft-front/src/components/Organism/ClientGroup/ClientGroupFormPresentation.vue @@ -0,0 +1,105 @@ + + + diff --git a/thanasoft-front/src/components/Organism/ClientGroup/ClientGroupListPresentation.vue b/thanasoft-front/src/components/Organism/ClientGroup/ClientGroupListPresentation.vue new file mode 100644 index 0000000..ae94eab --- /dev/null +++ b/thanasoft-front/src/components/Organism/ClientGroup/ClientGroupListPresentation.vue @@ -0,0 +1,107 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Commande/CommandeDetailPresentation.vue b/thanasoft-front/src/components/Organism/Commande/CommandeDetailPresentation.vue new file mode 100644 index 0000000..3a71d10 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Commande/CommandeDetailPresentation.vue @@ -0,0 +1,803 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Commande/CommandeListPresentation.vue b/thanasoft-front/src/components/Organism/Commande/CommandeListPresentation.vue new file mode 100644 index 0000000..13b47bb --- /dev/null +++ b/thanasoft-front/src/components/Organism/Commande/CommandeListPresentation.vue @@ -0,0 +1,118 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Commande/NewCommandePresentation.vue b/thanasoft-front/src/components/Organism/Commande/NewCommandePresentation.vue new file mode 100644 index 0000000..cd538d8 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Commande/NewCommandePresentation.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Convoys/AddConvoyPresentation.vue b/thanasoft-front/src/components/Organism/Convoys/AddConvoyPresentation.vue new file mode 100644 index 0000000..78c56e1 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Convoys/AddConvoyPresentation.vue @@ -0,0 +1,23 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Convoys/ConvoyListPresentation.vue b/thanasoft-front/src/components/Organism/Convoys/ConvoyListPresentation.vue new file mode 100644 index 0000000..6c0537b --- /dev/null +++ b/thanasoft-front/src/components/Organism/Convoys/ConvoyListPresentation.vue @@ -0,0 +1,157 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Defunts/AddDefuntPresentation.vue b/thanasoft-front/src/components/Organism/Defunts/AddDefuntPresentation.vue new file mode 100644 index 0000000..fbfb045 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Defunts/AddDefuntPresentation.vue @@ -0,0 +1,38 @@ + + diff --git a/thanasoft-front/src/components/Organism/Defunts/DefuntDetailPresentation.vue b/thanasoft-front/src/components/Organism/Defunts/DefuntDetailPresentation.vue new file mode 100644 index 0000000..db180b0 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Defunts/DefuntDetailPresentation.vue @@ -0,0 +1,68 @@ + + diff --git a/thanasoft-front/src/components/Organism/Defunts/DefuntPresentation.vue b/thanasoft-front/src/components/Organism/Defunts/DefuntPresentation.vue new file mode 100644 index 0000000..6fb0ce7 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Defunts/DefuntPresentation.vue @@ -0,0 +1,174 @@ + + + + diff --git a/thanasoft-front/src/components/Organism/Employee/AddVehiclePresentation.vue b/thanasoft-front/src/components/Organism/Employee/AddVehiclePresentation.vue new file mode 100644 index 0000000..7ba3311 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Employee/AddVehiclePresentation.vue @@ -0,0 +1,40 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Employee/EmployeePresentation.vue b/thanasoft-front/src/components/Organism/Employee/EmployeePresentation.vue new file mode 100644 index 0000000..97315ee --- /dev/null +++ b/thanasoft-front/src/components/Organism/Employee/EmployeePresentation.vue @@ -0,0 +1,95 @@ + + diff --git a/thanasoft-front/src/components/Organism/Employee/VehicleDetailPresentation.vue b/thanasoft-front/src/components/Organism/Employee/VehicleDetailPresentation.vue new file mode 100644 index 0000000..e9372b8 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Employee/VehicleDetailPresentation.vue @@ -0,0 +1,664 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Employee/VehiclePresentation.vue b/thanasoft-front/src/components/Organism/Employee/VehiclePresentation.vue new file mode 100644 index 0000000..16a3ecc --- /dev/null +++ b/thanasoft-front/src/components/Organism/Employee/VehiclePresentation.vue @@ -0,0 +1,70 @@ + + + diff --git a/thanasoft-front/src/components/Organism/FactureFournisseur/FactureFournisseurDetailPresentation.vue b/thanasoft-front/src/components/Organism/FactureFournisseur/FactureFournisseurDetailPresentation.vue new file mode 100644 index 0000000..e2736de --- /dev/null +++ b/thanasoft-front/src/components/Organism/FactureFournisseur/FactureFournisseurDetailPresentation.vue @@ -0,0 +1,96 @@ + + + diff --git a/thanasoft-front/src/components/Organism/FactureFournisseur/FactureFournisseurListPresentation.vue b/thanasoft-front/src/components/Organism/FactureFournisseur/FactureFournisseurListPresentation.vue new file mode 100644 index 0000000..a92ebd4 --- /dev/null +++ b/thanasoft-front/src/components/Organism/FactureFournisseur/FactureFournisseurListPresentation.vue @@ -0,0 +1,60 @@ + + + diff --git a/thanasoft-front/src/components/Organism/FactureFournisseur/NewFactureFournisseurPresentation.vue b/thanasoft-front/src/components/Organism/FactureFournisseur/NewFactureFournisseurPresentation.vue new file mode 100644 index 0000000..f87dde8 --- /dev/null +++ b/thanasoft-front/src/components/Organism/FactureFournisseur/NewFactureFournisseurPresentation.vue @@ -0,0 +1,44 @@ + + + diff --git a/thanasoft-front/src/components/Organism/InternalMessages/InternalMessagePresentation.vue b/thanasoft-front/src/components/Organism/InternalMessages/InternalMessagePresentation.vue new file mode 100644 index 0000000..2dc6c14 --- /dev/null +++ b/thanasoft-front/src/components/Organism/InternalMessages/InternalMessagePresentation.vue @@ -0,0 +1,327 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Interventions/AddInterventionPresentation.vue b/thanasoft-front/src/components/Organism/Interventions/AddInterventionPresentation.vue new file mode 100644 index 0000000..fe8cb66 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Interventions/AddInterventionPresentation.vue @@ -0,0 +1,225 @@ + + + + diff --git a/thanasoft-front/src/components/Organism/Interventions/InterventionDetailPresentation.vue b/thanasoft-front/src/components/Organism/Interventions/InterventionDetailPresentation.vue new file mode 100644 index 0000000..347496c --- /dev/null +++ b/thanasoft-front/src/components/Organism/Interventions/InterventionDetailPresentation.vue @@ -0,0 +1,213 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Interventions/InterventionPresentation.vue b/thanasoft-front/src/components/Organism/Interventions/InterventionPresentation.vue new file mode 100644 index 0000000..7080bde --- /dev/null +++ b/thanasoft-front/src/components/Organism/Interventions/InterventionPresentation.vue @@ -0,0 +1,220 @@ + + diff --git a/thanasoft-front/src/components/Organism/Interventions/intervention/AddPractitionerModal.vue b/thanasoft-front/src/components/Organism/Interventions/intervention/AddPractitionerModal.vue new file mode 100644 index 0000000..8a8f9bc --- /dev/null +++ b/thanasoft-front/src/components/Organism/Interventions/intervention/AddPractitionerModal.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Interventions/intervention/InterventionDetailContent.vue b/thanasoft-front/src/components/Organism/Interventions/intervention/InterventionDetailContent.vue new file mode 100644 index 0000000..07a095d --- /dev/null +++ b/thanasoft-front/src/components/Organism/Interventions/intervention/InterventionDetailContent.vue @@ -0,0 +1,665 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Interventions/intervention/InterventionDetailSidebar.vue b/thanasoft-front/src/components/Organism/Interventions/intervention/InterventionDetailSidebar.vue new file mode 100644 index 0000000..901eed7 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Interventions/intervention/InterventionDetailSidebar.vue @@ -0,0 +1,184 @@ + + + + + + diff --git a/thanasoft-front/src/components/Organism/Invoice/InvoiceDetailPresentation.vue b/thanasoft-front/src/components/Organism/Invoice/InvoiceDetailPresentation.vue new file mode 100644 index 0000000..f5f0b7d --- /dev/null +++ b/thanasoft-front/src/components/Organism/Invoice/InvoiceDetailPresentation.vue @@ -0,0 +1,396 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Invoice/InvoiceListPresentation.vue b/thanasoft-front/src/components/Organism/Invoice/InvoiceListPresentation.vue new file mode 100644 index 0000000..d29ffbf --- /dev/null +++ b/thanasoft-front/src/components/Organism/Invoice/InvoiceListPresentation.vue @@ -0,0 +1,46 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Location/LocationManager.vue b/thanasoft-front/src/components/Organism/Location/LocationManager.vue new file mode 100644 index 0000000..b0e4da7 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Location/LocationManager.vue @@ -0,0 +1,338 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Parametrage/Emails/EmailSettingsPresentation.vue b/thanasoft-front/src/components/Organism/Parametrage/Emails/EmailSettingsPresentation.vue new file mode 100644 index 0000000..72bc9ba --- /dev/null +++ b/thanasoft-front/src/components/Organism/Parametrage/Emails/EmailSettingsPresentation.vue @@ -0,0 +1,234 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Parametrage/ProductCategories/ProductCategoryList.vue b/thanasoft-front/src/components/Organism/Parametrage/ProductCategories/ProductCategoryList.vue new file mode 100644 index 0000000..c2be3ec --- /dev/null +++ b/thanasoft-front/src/components/Organism/Parametrage/ProductCategories/ProductCategoryList.vue @@ -0,0 +1,130 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Parametrage/ProductCategories/ProductCategoryModal.vue b/thanasoft-front/src/components/Organism/Parametrage/ProductCategories/ProductCategoryModal.vue new file mode 100644 index 0000000..4ccc726 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Parametrage/ProductCategories/ProductCategoryModal.vue @@ -0,0 +1,240 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Parametrage/Users/UserCreatePresentation.vue b/thanasoft-front/src/components/Organism/Parametrage/Users/UserCreatePresentation.vue new file mode 100644 index 0000000..29fa3c5 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Parametrage/Users/UserCreatePresentation.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Parametrage/Users/UserDetailPresentation.vue b/thanasoft-front/src/components/Organism/Parametrage/Users/UserDetailPresentation.vue new file mode 100644 index 0000000..8337232 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Parametrage/Users/UserDetailPresentation.vue @@ -0,0 +1,497 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Parametrage/Users/UserListPresentation.vue b/thanasoft-front/src/components/Organism/Parametrage/Users/UserListPresentation.vue new file mode 100644 index 0000000..8fe2229 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Parametrage/Users/UserListPresentation.vue @@ -0,0 +1,336 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Parametrage/Users/UserManagementPresentation.vue b/thanasoft-front/src/components/Organism/Parametrage/Users/UserManagementPresentation.vue new file mode 100644 index 0000000..06930f0 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Parametrage/Users/UserManagementPresentation.vue @@ -0,0 +1,320 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Planning/PlanningNewRequestModal.vue b/thanasoft-front/src/components/Organism/Planning/PlanningNewRequestModal.vue new file mode 100644 index 0000000..50ad53c --- /dev/null +++ b/thanasoft-front/src/components/Organism/Planning/PlanningNewRequestModal.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Planning/PlanningPresentation.vue b/thanasoft-front/src/components/Organism/Planning/PlanningPresentation.vue new file mode 100644 index 0000000..9967872 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Planning/PlanningPresentation.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Product/ProductDetailsSection.vue b/thanasoft-front/src/components/Organism/Product/ProductDetailsSection.vue new file mode 100644 index 0000000..c5c854f --- /dev/null +++ b/thanasoft-front/src/components/Organism/Product/ProductDetailsSection.vue @@ -0,0 +1,609 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Quote/QuoteCreationPresentation.vue b/thanasoft-front/src/components/Organism/Quote/QuoteCreationPresentation.vue new file mode 100644 index 0000000..756df6b --- /dev/null +++ b/thanasoft-front/src/components/Organism/Quote/QuoteCreationPresentation.vue @@ -0,0 +1,717 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Quote/QuoteDetailPresentation.vue b/thanasoft-front/src/components/Organism/Quote/QuoteDetailPresentation.vue new file mode 100644 index 0000000..21ba595 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Quote/QuoteDetailPresentation.vue @@ -0,0 +1,419 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Quote/QuoteListPresentation.vue b/thanasoft-front/src/components/Organism/Quote/QuoteListPresentation.vue new file mode 100644 index 0000000..bce59e0 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Quote/QuoteListPresentation.vue @@ -0,0 +1,51 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Statistics/StatisticsPresentation.vue b/thanasoft-front/src/components/Organism/Statistics/StatisticsPresentation.vue new file mode 100644 index 0000000..3151df4 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Statistics/StatisticsPresentation.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Stock/AddProductPresentation.vue b/thanasoft-front/src/components/Organism/Stock/AddProductPresentation.vue new file mode 100644 index 0000000..1d116ab --- /dev/null +++ b/thanasoft-front/src/components/Organism/Stock/AddProductPresentation.vue @@ -0,0 +1,612 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Stock/GoodsReceiptListPresentation.vue b/thanasoft-front/src/components/Organism/Stock/GoodsReceiptListPresentation.vue new file mode 100644 index 0000000..13aeeef --- /dev/null +++ b/thanasoft-front/src/components/Organism/Stock/GoodsReceiptListPresentation.vue @@ -0,0 +1,122 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Stock/NewReceptionPresentation.vue b/thanasoft-front/src/components/Organism/Stock/NewReceptionPresentation.vue new file mode 100644 index 0000000..4c78983 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Stock/NewReceptionPresentation.vue @@ -0,0 +1,71 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Stock/ProductCategoryPresentation.vue b/thanasoft-front/src/components/Organism/Stock/ProductCategoryPresentation.vue new file mode 100644 index 0000000..e211444 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Stock/ProductCategoryPresentation.vue @@ -0,0 +1,175 @@ + + diff --git a/thanasoft-front/src/components/Organism/Stock/ProductDetailsPresentation.vue b/thanasoft-front/src/components/Organism/Stock/ProductDetailsPresentation.vue new file mode 100644 index 0000000..0f66251 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Stock/ProductDetailsPresentation.vue @@ -0,0 +1,975 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Stock/ProductPresentation.vue b/thanasoft-front/src/components/Organism/Stock/ProductPresentation.vue new file mode 100644 index 0000000..cf73b90 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Stock/ProductPresentation.vue @@ -0,0 +1,76 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Stock/ReceptionDetailPresentation.vue b/thanasoft-front/src/components/Organism/Stock/ReceptionDetailPresentation.vue new file mode 100644 index 0000000..b0d5c09 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Stock/ReceptionDetailPresentation.vue @@ -0,0 +1,415 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Stock/WarehouseDetailPresentation.vue b/thanasoft-front/src/components/Organism/Stock/WarehouseDetailPresentation.vue new file mode 100644 index 0000000..d9535a3 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Stock/WarehouseDetailPresentation.vue @@ -0,0 +1,682 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Stock/WarehouseFormPresentation.vue b/thanasoft-front/src/components/Organism/Stock/WarehouseFormPresentation.vue new file mode 100644 index 0000000..bbe9959 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Stock/WarehouseFormPresentation.vue @@ -0,0 +1,99 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Stock/WarehouseListPresentation.vue b/thanasoft-front/src/components/Organism/Stock/WarehouseListPresentation.vue new file mode 100644 index 0000000..1578f6e --- /dev/null +++ b/thanasoft-front/src/components/Organism/Stock/WarehouseListPresentation.vue @@ -0,0 +1,65 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Thanatopractitioner/AddThanatopractitionerPresentation.vue b/thanasoft-front/src/components/Organism/Thanatopractitioner/AddThanatopractitionerPresentation.vue new file mode 100644 index 0000000..c169c3f --- /dev/null +++ b/thanasoft-front/src/components/Organism/Thanatopractitioner/AddThanatopractitionerPresentation.vue @@ -0,0 +1,501 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Thanatopractitioner/ThanatopractitionerDetailContent.vue b/thanasoft-front/src/components/Organism/Thanatopractitioner/ThanatopractitionerDetailContent.vue new file mode 100644 index 0000000..f6fda9d --- /dev/null +++ b/thanasoft-front/src/components/Organism/Thanatopractitioner/ThanatopractitionerDetailContent.vue @@ -0,0 +1,80 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Thanatopractitioner/ThanatopractitionerDetailPresentation.vue b/thanasoft-front/src/components/Organism/Thanatopractitioner/ThanatopractitionerDetailPresentation.vue new file mode 100644 index 0000000..3473e25 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Thanatopractitioner/ThanatopractitionerDetailPresentation.vue @@ -0,0 +1,167 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Thanatopractitioner/ThanatopractitionerDetailSidebar.vue b/thanasoft-front/src/components/Organism/Thanatopractitioner/ThanatopractitionerDetailSidebar.vue new file mode 100644 index 0000000..0358aad --- /dev/null +++ b/thanasoft-front/src/components/Organism/Thanatopractitioner/ThanatopractitionerDetailSidebar.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/thanasoft-front/src/components/Organism/Thanatopractitioner/ThanatopractitionerPresentation.vue b/thanasoft-front/src/components/Organism/Thanatopractitioner/ThanatopractitionerPresentation.vue new file mode 100644 index 0000000..55dc242 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Thanatopractitioner/ThanatopractitionerPresentation.vue @@ -0,0 +1,74 @@ + + diff --git a/thanasoft-front/src/components/Organism/Thanatopractitioner/thanatopractitioner/ThanatopractitionerDetailContent.vue b/thanasoft-front/src/components/Organism/Thanatopractitioner/thanatopractitioner/ThanatopractitionerDetailContent.vue new file mode 100644 index 0000000..a110e4f --- /dev/null +++ b/thanasoft-front/src/components/Organism/Thanatopractitioner/thanatopractitioner/ThanatopractitionerDetailContent.vue @@ -0,0 +1,231 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Thanatopractitioner/thanatopractitioner/ThanatopractitionerDetailSidebar.vue b/thanasoft-front/src/components/Organism/Thanatopractitioner/thanatopractitioner/ThanatopractitionerDetailSidebar.vue new file mode 100644 index 0000000..6f8627a --- /dev/null +++ b/thanasoft-front/src/components/Organism/Thanatopractitioner/thanatopractitioner/ThanatopractitionerDetailSidebar.vue @@ -0,0 +1,147 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Webmailing/WebmailingPresentation.vue b/thanasoft-front/src/components/Organism/Webmailing/WebmailingPresentation.vue new file mode 100644 index 0000000..6237036 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Webmailing/WebmailingPresentation.vue @@ -0,0 +1,1362 @@ + + + + + diff --git a/thanasoft-front/src/components/SoftButton.vue b/thanasoft-front/src/components/SoftButton.vue index 0caaf39..11507c4 100644 --- a/thanasoft-front/src/components/SoftButton.vue +++ b/thanasoft-front/src/components/SoftButton.vue @@ -2,6 +2,7 @@ @@ -10,6 +11,7 @@ diff --git a/thanasoft-front/src/components/SoftTextarea.vue b/thanasoft-front/src/components/SoftTextarea.vue index 711efe2..e69de29 100644 --- a/thanasoft-front/src/components/SoftTextarea.vue +++ b/thanasoft-front/src/components/SoftTextarea.vue @@ -1,29 +0,0 @@ - - - diff --git a/thanasoft-front/src/components/atoms/Agenda/AddInterventionButton.vue b/thanasoft-front/src/components/atoms/Agenda/AddInterventionButton.vue new file mode 100644 index 0000000..4b32b2d --- /dev/null +++ b/thanasoft-front/src/components/atoms/Agenda/AddInterventionButton.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/thanasoft-front/src/components/atoms/Agenda/InterventionBadge.vue b/thanasoft-front/src/components/atoms/Agenda/InterventionBadge.vue new file mode 100644 index 0000000..8cba9af --- /dev/null +++ b/thanasoft-front/src/components/atoms/Agenda/InterventionBadge.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/thanasoft-front/src/components/atoms/Defunts/DefuntCard.vue b/thanasoft-front/src/components/atoms/Defunts/DefuntCard.vue new file mode 100644 index 0000000..244526f --- /dev/null +++ b/thanasoft-front/src/components/atoms/Defunts/DefuntCard.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/thanasoft-front/src/components/atoms/InternalMessages/MessageContent.vue b/thanasoft-front/src/components/atoms/InternalMessages/MessageContent.vue new file mode 100644 index 0000000..ef56bf3 --- /dev/null +++ b/thanasoft-front/src/components/atoms/InternalMessages/MessageContent.vue @@ -0,0 +1,44 @@ + + + diff --git a/thanasoft-front/src/components/atoms/InternalMessages/MessageTypeSelect.vue b/thanasoft-front/src/components/atoms/InternalMessages/MessageTypeSelect.vue new file mode 100644 index 0000000..aa2b048 --- /dev/null +++ b/thanasoft-front/src/components/atoms/InternalMessages/MessageTypeSelect.vue @@ -0,0 +1,47 @@ + + + diff --git a/thanasoft-front/src/components/atoms/InternalMessages/RecipientSelect.vue b/thanasoft-front/src/components/atoms/InternalMessages/RecipientSelect.vue new file mode 100644 index 0000000..08f66cd --- /dev/null +++ b/thanasoft-front/src/components/atoms/InternalMessages/RecipientSelect.vue @@ -0,0 +1,47 @@ + + + diff --git a/thanasoft-front/src/components/atoms/Interventions/CardInterventions.vue b/thanasoft-front/src/components/atoms/Interventions/CardInterventions.vue new file mode 100644 index 0000000..2ab1779 --- /dev/null +++ b/thanasoft-front/src/components/atoms/Interventions/CardInterventions.vue @@ -0,0 +1,118 @@ + + + diff --git a/thanasoft-front/src/components/atoms/Location/LocationDisplay.vue b/thanasoft-front/src/components/atoms/Location/LocationDisplay.vue new file mode 100644 index 0000000..9a0f1c1 --- /dev/null +++ b/thanasoft-front/src/components/atoms/Location/LocationDisplay.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/thanasoft-front/src/components/atoms/Location/LocationSearchInput.vue b/thanasoft-front/src/components/atoms/Location/LocationSearchInput.vue new file mode 100644 index 0000000..040e32f --- /dev/null +++ b/thanasoft-front/src/components/atoms/Location/LocationSearchInput.vue @@ -0,0 +1,265 @@ + + + + + diff --git a/thanasoft-front/src/components/atoms/Planning/PlanningActionButton.vue b/thanasoft-front/src/components/atoms/Planning/PlanningActionButton.vue new file mode 100644 index 0000000..07f7511 --- /dev/null +++ b/thanasoft-front/src/components/atoms/Planning/PlanningActionButton.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/thanasoft-front/src/components/atoms/Product/FieldDisplay.vue b/thanasoft-front/src/components/atoms/Product/FieldDisplay.vue new file mode 100644 index 0000000..9d1abc8 --- /dev/null +++ b/thanasoft-front/src/components/atoms/Product/FieldDisplay.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/thanasoft-front/src/components/atoms/Product/FieldInput.vue b/thanasoft-front/src/components/atoms/Product/FieldInput.vue new file mode 100644 index 0000000..2aaa8f7 --- /dev/null +++ b/thanasoft-front/src/components/atoms/Product/FieldInput.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Agenda/DocumentsStep.vue b/thanasoft-front/src/components/molecules/Agenda/DocumentsStep.vue new file mode 100644 index 0000000..5427c15 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Agenda/DocumentsStep.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Agenda/InterventionStep.vue b/thanasoft-front/src/components/molecules/Agenda/InterventionStep.vue new file mode 100644 index 0000000..899ffcf --- /dev/null +++ b/thanasoft-front/src/components/molecules/Agenda/InterventionStep.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Agenda/LocationStep.vue b/thanasoft-front/src/components/molecules/Agenda/LocationStep.vue new file mode 100644 index 0000000..4aa1347 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Agenda/LocationStep.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Agenda/UpcomingInterventions.vue b/thanasoft-front/src/components/molecules/Agenda/UpcomingInterventions.vue new file mode 100644 index 0000000..f41af96 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Agenda/UpcomingInterventions.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Avoir/AvoirHeader.vue b/thanasoft-front/src/components/molecules/Avoir/AvoirHeader.vue new file mode 100644 index 0000000..0d3f825 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Avoir/AvoirHeader.vue @@ -0,0 +1,34 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Avoir/AvoirLinesTable.vue b/thanasoft-front/src/components/molecules/Avoir/AvoirLinesTable.vue new file mode 100644 index 0000000..b5ec6ce --- /dev/null +++ b/thanasoft-front/src/components/molecules/Avoir/AvoirLinesTable.vue @@ -0,0 +1,47 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Avoir/AvoirListControls.vue b/thanasoft-front/src/components/molecules/Avoir/AvoirListControls.vue new file mode 100644 index 0000000..0f7b3dd --- /dev/null +++ b/thanasoft-front/src/components/molecules/Avoir/AvoirListControls.vue @@ -0,0 +1,95 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Avoir/AvoirSummary.vue b/thanasoft-front/src/components/molecules/Avoir/AvoirSummary.vue new file mode 100644 index 0000000..9b8e057 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Avoir/AvoirSummary.vue @@ -0,0 +1,49 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Avoir/NewAvoirForm.vue b/thanasoft-front/src/components/molecules/Avoir/NewAvoirForm.vue new file mode 100644 index 0000000..81cd2dc --- /dev/null +++ b/thanasoft-front/src/components/molecules/Avoir/NewAvoirForm.vue @@ -0,0 +1,573 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/ClientGroup/ClientGroupForm.vue b/thanasoft-front/src/components/molecules/ClientGroup/ClientGroupForm.vue new file mode 100644 index 0000000..d9c39e5 --- /dev/null +++ b/thanasoft-front/src/components/molecules/ClientGroup/ClientGroupForm.vue @@ -0,0 +1,177 @@ + + + diff --git a/thanasoft-front/src/components/molecules/ClientGroup/ClientGroupListControls.vue b/thanasoft-front/src/components/molecules/ClientGroup/ClientGroupListControls.vue new file mode 100644 index 0000000..fdb802d --- /dev/null +++ b/thanasoft-front/src/components/molecules/ClientGroup/ClientGroupListControls.vue @@ -0,0 +1,30 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Commande/CommandeHeader.vue b/thanasoft-front/src/components/molecules/Commande/CommandeHeader.vue new file mode 100644 index 0000000..60050ba --- /dev/null +++ b/thanasoft-front/src/components/molecules/Commande/CommandeHeader.vue @@ -0,0 +1,30 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Commande/CommandeLinesTable.vue b/thanasoft-front/src/components/molecules/Commande/CommandeLinesTable.vue new file mode 100644 index 0000000..d0c3352 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Commande/CommandeLinesTable.vue @@ -0,0 +1,47 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Commande/CommandeSummary.vue b/thanasoft-front/src/components/molecules/Commande/CommandeSummary.vue new file mode 100644 index 0000000..dff786c --- /dev/null +++ b/thanasoft-front/src/components/molecules/Commande/CommandeSummary.vue @@ -0,0 +1,51 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Commande/NewCommandeForm.vue b/thanasoft-front/src/components/molecules/Commande/NewCommandeForm.vue new file mode 100644 index 0000000..603b2c6 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Commande/NewCommandeForm.vue @@ -0,0 +1,1285 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Convoy/ConvoyEventCard.vue b/thanasoft-front/src/components/molecules/Convoy/ConvoyEventCard.vue new file mode 100644 index 0000000..8932bcf --- /dev/null +++ b/thanasoft-front/src/components/molecules/Convoy/ConvoyEventCard.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Defunts/DefuntDetailContent.vue b/thanasoft-front/src/components/molecules/Defunts/DefuntDetailContent.vue new file mode 100644 index 0000000..9dcb960 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Defunts/DefuntDetailContent.vue @@ -0,0 +1,643 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Defunts/DefuntDetailSidebar.vue b/thanasoft-front/src/components/molecules/Defunts/DefuntDetailSidebar.vue new file mode 100644 index 0000000..feca2b2 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Defunts/DefuntDetailSidebar.vue @@ -0,0 +1,178 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Defunts/DefuntForm.vue b/thanasoft-front/src/components/molecules/Defunts/DefuntForm.vue new file mode 100644 index 0000000..6afdfff --- /dev/null +++ b/thanasoft-front/src/components/molecules/Defunts/DefuntForm.vue @@ -0,0 +1,310 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Defunts/DefuntsList.vue b/thanasoft-front/src/components/molecules/Defunts/DefuntsList.vue new file mode 100644 index 0000000..6c8f82a --- /dev/null +++ b/thanasoft-front/src/components/molecules/Defunts/DefuntsList.vue @@ -0,0 +1,395 @@ + + + + diff --git a/thanasoft-front/src/components/molecules/Employees/EmployeeTable.vue b/thanasoft-front/src/components/molecules/Employees/EmployeeTable.vue new file mode 100644 index 0000000..963bb3d --- /dev/null +++ b/thanasoft-front/src/components/molecules/Employees/EmployeeTable.vue @@ -0,0 +1,667 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/FactureFournisseur/FactureFournisseurHeader.vue b/thanasoft-front/src/components/molecules/FactureFournisseur/FactureFournisseurHeader.vue new file mode 100644 index 0000000..c4ab51b --- /dev/null +++ b/thanasoft-front/src/components/molecules/FactureFournisseur/FactureFournisseurHeader.vue @@ -0,0 +1,36 @@ + + + diff --git a/thanasoft-front/src/components/molecules/FactureFournisseur/FactureFournisseurLinesTable.vue b/thanasoft-front/src/components/molecules/FactureFournisseur/FactureFournisseurLinesTable.vue new file mode 100644 index 0000000..d5ef7ef --- /dev/null +++ b/thanasoft-front/src/components/molecules/FactureFournisseur/FactureFournisseurLinesTable.vue @@ -0,0 +1,75 @@ + + + diff --git a/thanasoft-front/src/components/molecules/FactureFournisseur/FactureFournisseurListControls.vue b/thanasoft-front/src/components/molecules/FactureFournisseur/FactureFournisseurListControls.vue new file mode 100644 index 0000000..6e286cd --- /dev/null +++ b/thanasoft-front/src/components/molecules/FactureFournisseur/FactureFournisseurListControls.vue @@ -0,0 +1,92 @@ + + + diff --git a/thanasoft-front/src/components/molecules/FactureFournisseur/FactureFournisseurSummary.vue b/thanasoft-front/src/components/molecules/FactureFournisseur/FactureFournisseurSummary.vue new file mode 100644 index 0000000..59c88f1 --- /dev/null +++ b/thanasoft-front/src/components/molecules/FactureFournisseur/FactureFournisseurSummary.vue @@ -0,0 +1,70 @@ + + + diff --git a/thanasoft-front/src/components/molecules/FactureFournisseur/NewFactureFournisseurForm.vue b/thanasoft-front/src/components/molecules/FactureFournisseur/NewFactureFournisseurForm.vue new file mode 100644 index 0000000..6172a34 --- /dev/null +++ b/thanasoft-front/src/components/molecules/FactureFournisseur/NewFactureFournisseurForm.vue @@ -0,0 +1,360 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/InternalMessages/InternalMessageForm.vue b/thanasoft-front/src/components/molecules/InternalMessages/InternalMessageForm.vue new file mode 100644 index 0000000..5ce5ead --- /dev/null +++ b/thanasoft-front/src/components/molecules/InternalMessages/InternalMessageForm.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/InternalMessages/InternalMessageList.vue b/thanasoft-front/src/components/molecules/InternalMessages/InternalMessageList.vue new file mode 100644 index 0000000..7e578b8 --- /dev/null +++ b/thanasoft-front/src/components/molecules/InternalMessages/InternalMessageList.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Interventions/DocumentManagement.vue b/thanasoft-front/src/components/molecules/Interventions/DocumentManagement.vue new file mode 100644 index 0000000..2432400 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Interventions/DocumentManagement.vue @@ -0,0 +1,555 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Interventions/InterventationAddModal.vue b/thanasoft-front/src/components/molecules/Interventions/InterventationAddModal.vue new file mode 100644 index 0000000..9bb8ac8 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Interventions/InterventationAddModal.vue @@ -0,0 +1,428 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Interventions/InterventionForm.vue b/thanasoft-front/src/components/molecules/Interventions/InterventionForm.vue new file mode 100644 index 0000000..9645755 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Interventions/InterventionForm.vue @@ -0,0 +1,691 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Interventions/interventionDetails.vue b/thanasoft-front/src/components/molecules/Interventions/interventionDetails.vue new file mode 100644 index 0000000..5dd8b26 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Interventions/interventionDetails.vue @@ -0,0 +1,573 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Interventions/interventionsList.vue b/thanasoft-front/src/components/molecules/Interventions/interventionsList.vue new file mode 100644 index 0000000..47379bb --- /dev/null +++ b/thanasoft-front/src/components/molecules/Interventions/interventionsList.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Invoice/InvoiceHeader.vue b/thanasoft-front/src/components/molecules/Invoice/InvoiceHeader.vue new file mode 100644 index 0000000..111e4b1 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Invoice/InvoiceHeader.vue @@ -0,0 +1,32 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Invoice/InvoiceLinesTable.vue b/thanasoft-front/src/components/molecules/Invoice/InvoiceLinesTable.vue new file mode 100644 index 0000000..86a8475 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Invoice/InvoiceLinesTable.vue @@ -0,0 +1,97 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Invoice/InvoiceListControls.vue b/thanasoft-front/src/components/molecules/Invoice/InvoiceListControls.vue new file mode 100644 index 0000000..849aad5 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Invoice/InvoiceListControls.vue @@ -0,0 +1,104 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Invoice/InvoiceSummary.vue b/thanasoft-front/src/components/molecules/Invoice/InvoiceSummary.vue new file mode 100644 index 0000000..2739d2a --- /dev/null +++ b/thanasoft-front/src/components/molecules/Invoice/InvoiceSummary.vue @@ -0,0 +1,46 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Invoice/InvoiceTimeline.vue b/thanasoft-front/src/components/molecules/Invoice/InvoiceTimeline.vue new file mode 100644 index 0000000..158f4a0 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Invoice/InvoiceTimeline.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Planning/PlanningCollaboratorsSidebar.vue b/thanasoft-front/src/components/molecules/Planning/PlanningCollaboratorsSidebar.vue new file mode 100644 index 0000000..1f455c0 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Planning/PlanningCollaboratorsSidebar.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Planning/PlanningCreationTypeSelector.vue b/thanasoft-front/src/components/molecules/Planning/PlanningCreationTypeSelector.vue new file mode 100644 index 0000000..b871ed6 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Planning/PlanningCreationTypeSelector.vue @@ -0,0 +1,22 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Planning/PlanningDateNavigator.vue b/thanasoft-front/src/components/molecules/Planning/PlanningDateNavigator.vue new file mode 100644 index 0000000..a8018e1 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Planning/PlanningDateNavigator.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Planning/PlanningEventForm.vue b/thanasoft-front/src/components/molecules/Planning/PlanningEventForm.vue new file mode 100644 index 0000000..5a6ba19 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Planning/PlanningEventForm.vue @@ -0,0 +1,68 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Planning/PlanningKanban.vue b/thanasoft-front/src/components/molecules/Planning/PlanningKanban.vue new file mode 100644 index 0000000..d1b4f78 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Planning/PlanningKanban.vue @@ -0,0 +1,724 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Planning/PlanningLeaveRequestForm.vue b/thanasoft-front/src/components/molecules/Planning/PlanningLeaveRequestForm.vue new file mode 100644 index 0000000..1718414 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Planning/PlanningLeaveRequestForm.vue @@ -0,0 +1,80 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Planning/PlanningLegend.vue b/thanasoft-front/src/components/molecules/Planning/PlanningLegend.vue new file mode 100644 index 0000000..685b862 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Planning/PlanningLegend.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Planning/PlanningList.vue b/thanasoft-front/src/components/molecules/Planning/PlanningList.vue new file mode 100644 index 0000000..d2d646d --- /dev/null +++ b/thanasoft-front/src/components/molecules/Planning/PlanningList.vue @@ -0,0 +1,208 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Planning/PlanningViewToggles.vue b/thanasoft-front/src/components/molecules/Planning/PlanningViewToggles.vue new file mode 100644 index 0000000..dd90312 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Planning/PlanningViewToggles.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Planning/PlanningWeekGrid.vue b/thanasoft-front/src/components/molecules/Planning/PlanningWeekGrid.vue new file mode 100644 index 0000000..1c6a730 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Planning/PlanningWeekGrid.vue @@ -0,0 +1,743 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Product/ProductInfoGrid.vue b/thanasoft-front/src/components/molecules/Product/ProductInfoGrid.vue new file mode 100644 index 0000000..7bc7b1a --- /dev/null +++ b/thanasoft-front/src/components/molecules/Product/ProductInfoGrid.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Product/ProductInfoSection.vue b/thanasoft-front/src/components/molecules/Product/ProductInfoSection.vue new file mode 100644 index 0000000..fbc4d40 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Product/ProductInfoSection.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Product/ProductMovementsSection.vue b/thanasoft-front/src/components/molecules/Product/ProductMovementsSection.vue new file mode 100644 index 0000000..1950a86 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Product/ProductMovementsSection.vue @@ -0,0 +1,178 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Product/ProductSidebar.vue b/thanasoft-front/src/components/molecules/Product/ProductSidebar.vue new file mode 100644 index 0000000..7f97d7e --- /dev/null +++ b/thanasoft-front/src/components/molecules/Product/ProductSidebar.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Product/ProductStockSection.vue b/thanasoft-front/src/components/molecules/Product/ProductStockSection.vue new file mode 100644 index 0000000..7f62912 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Product/ProductStockSection.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Product/ProductSupplierSection.vue b/thanasoft-front/src/components/molecules/Product/ProductSupplierSection.vue new file mode 100644 index 0000000..a838dcb --- /dev/null +++ b/thanasoft-front/src/components/molecules/Product/ProductSupplierSection.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Product/StockPricingInfo.vue b/thanasoft-front/src/components/molecules/Product/StockPricingInfo.vue new file mode 100644 index 0000000..28cc1ce --- /dev/null +++ b/thanasoft-front/src/components/molecules/Product/StockPricingInfo.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Product/SupplierInfo.vue b/thanasoft-front/src/components/molecules/Product/SupplierInfo.vue new file mode 100644 index 0000000..8daa2d2 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Product/SupplierInfo.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Quote/ProductLineItem.vue b/thanasoft-front/src/components/molecules/Quote/ProductLineItem.vue new file mode 100644 index 0000000..1b5e637 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Quote/ProductLineItem.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Quote/QuoteBillingInfo.vue b/thanasoft-front/src/components/molecules/Quote/QuoteBillingInfo.vue new file mode 100644 index 0000000..164c04e --- /dev/null +++ b/thanasoft-front/src/components/molecules/Quote/QuoteBillingInfo.vue @@ -0,0 +1,55 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Quote/QuoteHeader.vue b/thanasoft-front/src/components/molecules/Quote/QuoteHeader.vue new file mode 100644 index 0000000..95086fb --- /dev/null +++ b/thanasoft-front/src/components/molecules/Quote/QuoteHeader.vue @@ -0,0 +1,43 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Quote/QuoteInfoCard.vue b/thanasoft-front/src/components/molecules/Quote/QuoteInfoCard.vue new file mode 100644 index 0000000..27a69b8 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Quote/QuoteInfoCard.vue @@ -0,0 +1,80 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Quote/QuoteLinesTable.vue b/thanasoft-front/src/components/molecules/Quote/QuoteLinesTable.vue new file mode 100644 index 0000000..e64b3c4 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Quote/QuoteLinesTable.vue @@ -0,0 +1,103 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Quote/QuoteListControls.vue b/thanasoft-front/src/components/molecules/Quote/QuoteListControls.vue new file mode 100644 index 0000000..a4a328b --- /dev/null +++ b/thanasoft-front/src/components/molecules/Quote/QuoteListControls.vue @@ -0,0 +1,95 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Quote/QuoteSummary.vue b/thanasoft-front/src/components/molecules/Quote/QuoteSummary.vue new file mode 100644 index 0000000..2abb6d9 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Quote/QuoteSummary.vue @@ -0,0 +1,43 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Quote/QuoteTimeline.vue b/thanasoft-front/src/components/molecules/Quote/QuoteTimeline.vue new file mode 100644 index 0000000..3996c59 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Quote/QuoteTimeline.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Quote/QuoteTotalsCard.vue b/thanasoft-front/src/components/molecules/Quote/QuoteTotalsCard.vue new file mode 100644 index 0000000..babb729 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Quote/QuoteTotalsCard.vue @@ -0,0 +1,44 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Statistics/InterventionChart.vue b/thanasoft-front/src/components/molecules/Statistics/InterventionChart.vue new file mode 100644 index 0000000..658dce2 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Statistics/InterventionChart.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Statistics/StatisticsOverview.vue b/thanasoft-front/src/components/molecules/Statistics/StatisticsOverview.vue new file mode 100644 index 0000000..f2e9d77 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Statistics/StatisticsOverview.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Statistics/ThanatometerPerformance.vue b/thanasoft-front/src/components/molecules/Statistics/ThanatometerPerformance.vue new file mode 100644 index 0000000..b0dc37d --- /dev/null +++ b/thanasoft-front/src/components/molecules/Statistics/ThanatometerPerformance.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Stock/NewReceptionForm.vue b/thanasoft-front/src/components/molecules/Stock/NewReceptionForm.vue new file mode 100644 index 0000000..ad89c56 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Stock/NewReceptionForm.vue @@ -0,0 +1,1986 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Stock/ProductCategoryModal.vue b/thanasoft-front/src/components/molecules/Stock/ProductCategoryModal.vue new file mode 100644 index 0000000..63624f8 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Stock/ProductCategoryModal.vue @@ -0,0 +1,473 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Stock/WarehouseDetailInfo.vue b/thanasoft-front/src/components/molecules/Stock/WarehouseDetailInfo.vue new file mode 100644 index 0000000..b2a4a7d --- /dev/null +++ b/thanasoft-front/src/components/molecules/Stock/WarehouseDetailInfo.vue @@ -0,0 +1,39 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Stock/WarehouseForm.vue b/thanasoft-front/src/components/molecules/Stock/WarehouseForm.vue new file mode 100644 index 0000000..070de58 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Stock/WarehouseForm.vue @@ -0,0 +1,115 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Tables/Avoirs/AvoirTable.vue b/thanasoft-front/src/components/molecules/Tables/Avoirs/AvoirTable.vue new file mode 100644 index 0000000..075d75b --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/Avoirs/AvoirTable.vue @@ -0,0 +1,264 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Tables/CRM/ClientTable.vue b/thanasoft-front/src/components/molecules/Tables/CRM/ClientTable.vue new file mode 100644 index 0000000..201cbc3 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/CRM/ClientTable.vue @@ -0,0 +1,631 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Tables/CRM/FournisseurTable.vue b/thanasoft-front/src/components/molecules/Tables/CRM/FournisseurTable.vue new file mode 100644 index 0000000..75834bd --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/CRM/FournisseurTable.vue @@ -0,0 +1,489 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Tables/ClientGroup/ClientGroupTable.vue b/thanasoft-front/src/components/molecules/Tables/ClientGroup/ClientGroupTable.vue new file mode 100644 index 0000000..44bc46d --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/ClientGroup/ClientGroupTable.vue @@ -0,0 +1,555 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Tables/ContactTable.vue b/thanasoft-front/src/components/molecules/Tables/ContactTable.vue new file mode 100644 index 0000000..ca798c6 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/ContactTable.vue @@ -0,0 +1,447 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Tables/Employees/VehicleTable.vue b/thanasoft-front/src/components/molecules/Tables/Employees/VehicleTable.vue new file mode 100644 index 0000000..dc822c6 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/Employees/VehicleTable.vue @@ -0,0 +1,288 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Tables/FilterTable.vue b/thanasoft-front/src/components/molecules/Tables/FilterTable.vue new file mode 100644 index 0000000..866a706 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/FilterTable.vue @@ -0,0 +1,37 @@ + + diff --git a/thanasoft-front/src/components/molecules/Tables/Fournisseurs/CommandeTable.vue b/thanasoft-front/src/components/molecules/Tables/Fournisseurs/CommandeTable.vue new file mode 100644 index 0000000..f1d6951 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/Fournisseurs/CommandeTable.vue @@ -0,0 +1,266 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Tables/Fournisseurs/FactureFournisseurTable.vue b/thanasoft-front/src/components/molecules/Tables/Fournisseurs/FactureFournisseurTable.vue new file mode 100644 index 0000000..f27c3ad --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/Fournisseurs/FactureFournisseurTable.vue @@ -0,0 +1,223 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Tables/Stock/GoodsReceiptTable.vue b/thanasoft-front/src/components/molecules/Tables/Stock/GoodsReceiptTable.vue new file mode 100644 index 0000000..59cfd7b --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/Stock/GoodsReceiptTable.vue @@ -0,0 +1,207 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Tables/Stock/ProductCategoryTable.vue b/thanasoft-front/src/components/molecules/Tables/Stock/ProductCategoryTable.vue new file mode 100644 index 0000000..688ecc3 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/Stock/ProductCategoryTable.vue @@ -0,0 +1,532 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Tables/Stock/ProductTable.vue b/thanasoft-front/src/components/molecules/Tables/Stock/ProductTable.vue new file mode 100644 index 0000000..cb9e32d --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/Stock/ProductTable.vue @@ -0,0 +1,611 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Tables/Stock/WarehouseTable.vue b/thanasoft-front/src/components/molecules/Tables/Stock/WarehouseTable.vue new file mode 100644 index 0000000..e11d9ae --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/Stock/WarehouseTable.vue @@ -0,0 +1,174 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Tables/TableAction.vue b/thanasoft-front/src/components/molecules/Tables/TableAction.vue new file mode 100644 index 0000000..48c692a --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/TableAction.vue @@ -0,0 +1,17 @@ + + diff --git a/thanasoft-front/src/components/molecules/Tables/Ventes/InvoiceTable.vue b/thanasoft-front/src/components/molecules/Tables/Ventes/InvoiceTable.vue new file mode 100644 index 0000000..02bfea4 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/Ventes/InvoiceTable.vue @@ -0,0 +1,283 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Tables/Ventes/QuoteTable.vue b/thanasoft-front/src/components/molecules/Tables/Ventes/QuoteTable.vue new file mode 100644 index 0000000..4127d24 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/Ventes/QuoteTable.vue @@ -0,0 +1,282 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Thanatopractitioners/ThanatopractitionerTable.vue b/thanasoft-front/src/components/molecules/Thanatopractitioners/ThanatopractitionerTable.vue new file mode 100644 index 0000000..9c9aefa --- /dev/null +++ b/thanasoft-front/src/components/molecules/Thanatopractitioners/ThanatopractitionerTable.vue @@ -0,0 +1,586 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/auth/LoginForm.vue b/thanasoft-front/src/components/molecules/auth/LoginForm.vue index eb5358e..7989617 100644 --- a/thanasoft-front/src/components/molecules/auth/LoginForm.vue +++ b/thanasoft-front/src/components/molecules/auth/LoginForm.vue @@ -31,7 +31,11 @@ > Souvenez de moi -