Compare commits

..

5 Commits

Author SHA1 Message Date
09d63a68d8 Merge branch 'develop' into feat/front-end-layout-admin
# Conflicts:
#	.idea/workspace.xml
#	frontend/layouts/default.vue
2026-02-06 09:15:35 +01:00
0a4a021a46 feat : layout administration 2026-02-06 09:08:18 +01:00
47679926ff feat : layout administration 2026-02-06 08:13:31 +01:00
c186e6a5e1 feat : layout administration (WIP) 2026-02-05 16:47:01 +01:00
3062709f08 feat : layout administration (WIP) 2026-02-05 14:01:29 +01:00
28 changed files with 168 additions and 748 deletions

View File

@@ -16,50 +16,30 @@ jobs:
token: ${{ secrets.RELEASE_TOKEN }} token: ${{ secrets.RELEASE_TOKEN }}
persist-credentials: true persist-credentials: true
- name: Create next tag from config/version.yaml - name: Create next tag v0.0.X
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
# Skip if current commit already has a vX.Y.Z tag # Skip if current commit already has a v0.0.* tag
if git tag --points-at HEAD | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then if git tag --points-at HEAD | grep -qE '^v0\.0\.'; then
echo "Tag already exists on this commit. Skipping." echo "Tag already exists on this commit. Skipping."
exit 0 exit 0
fi fi
changed_version=false last_tag="$(git tag -l 'v0.0.*' --sort=-v:refname | head -n1 || true)"
if git diff --name-only "${{ gitea.event.before }}" "${{ gitea.event.after }}" | grep -q '^config/version\.yaml$'; then if [ -z "$last_tag" ]; then
changed_version=true next_tag="v0.0.1"
fi else
patch="${last_tag##v0.0.}"
read_version() { if ! [[ "$patch" =~ ^[0-9]+$ ]]; then
awk -F': *' '/app\.version:/{print $2}' config/version.yaml | tr -d '[:space:]' | tr -d "'\"" echo "Unexpected tag format: $last_tag" >&2
}
if $changed_version; then
version="$(read_version)"
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid version in version.yaml: $version" >&2
exit 1 exit 1
fi fi
else next_tag="v0.0.$((patch + 1))"
last_tag="$(git tag -l 'v*' --sort=-v:refname | head -n1 || true)"
if [ -z "$last_tag" ]; then
version="0.1.0"
else
base="${last_tag#v}"
IFS='.' read -r major minor patch <<< "$base"
version="${major}.${minor}.$((patch + 1))"
fi
printf "parameters:\\n app.version: '%s'\\n" "$version" > config/version.yaml
git config user.name "gitea-actions"
git config user.email "gitea-actions@local"
git add config/version.yaml
git commit -m "chore: bump version to v$version" || true
git push origin develop || true
fi fi
tag="v$version" git config user.name "gitea-actions"
git tag "$tag" git config user.email "gitea-actions@local"
git push origin "$tag" git tag "$next_tag"
git push origin "$next_tag"

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourcePerFileMappings">
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_1.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_2.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_3.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_4.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
</component>
</project>

139
.idea/workspace.xml generated
View File

@@ -4,11 +4,10 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="fix : panel scrollable plus interface revue"> <list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : ajout du responsive sur la navbar et la page d'accueil">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" /> <change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/pages/admin/carrier/carrier-list.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/admin/carrier/carrier-list.vue" afterDir="false" /> <change beforePath="$PROJECT_DIR$/frontend/layouts/default.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/layouts/default.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/pages/admin/supplier/supplier-list.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/admin/supplier/supplier-list.vue" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -227,7 +226,7 @@
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true", "RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.typescript.service.memoryLimit.init": "true", "RunOnceActivity.typescript.service.memoryLimit.init": "true",
"git-widget-placeholder": "feat/312-creation-d-une-page-d-administration-listing-des-fournisseurs", "git-widget-placeholder": "develop",
"last_opened_file_path": "/home/sroy/Documents/test/Ferme", "last_opened_file_path": "/home/sroy/Documents/test/Ferme",
"node.js.detected.package.eslint": "true", "node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true", "node.js.detected.package.tslint": "true",
@@ -252,11 +251,11 @@
}]]></component> }]]></component>
<component name="RecentsManager"> <component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS"> <key name="MoveFile.RECENT_KEYS">
<recent name="\\wsl.localhost\Ubuntu-24.04\home\matte\Ferme\frontend\pages\admin\supplier" />
<recent name="\\wsl.localhost\Ubuntu-24.04\home\m-tristan\workspace\Ferme" /> <recent name="\\wsl.localhost\Ubuntu-24.04\home\m-tristan\workspace\Ferme" />
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\templates" /> <recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\templates" />
<recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches" /> <recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches" />
<recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches\Ferme_MCD\MCD_DOC" /> <recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches\Ferme_MCD\MCD_DOC" />
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\frontend\pages\reception" />
</key> </key>
</component> </component>
<component name="SharedIndexes"> <component name="SharedIndexes">
@@ -297,6 +296,62 @@
<workItem from="1770195959162" duration="18915000" /> <workItem from="1770195959162" duration="18915000" />
<workItem from="1770274844804" duration="3940000" /> <workItem from="1770274844804" duration="3940000" />
</task> </task>
<task id="LOCAL-00003" summary="feat : Ajout d'un composable pour la pesée qui sera réutilisable pour l'expédition, ajout de contrainte sur les entity de reception et weight pour plus de robustesse et correction de la class active des liens dans la nav">
<option name="closed" value="true" />
<created>1768316835575</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1768316835575</updated>
</task>
<task id="LOCAL-00004" summary="feat : update du fichier AGENTS.md">
<option name="closed" value="true" />
<created>1768316965511</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1768316965511</updated>
</task>
<task id="LOCAL-00005" summary="feat : update du fichier README.md et CHANGELOG.md">
<option name="closed" value="true" />
<created>1768317786187</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1768317786187</updated>
</task>
<task id="LOCAL-00006" summary="fix : correction du useApi pour qu'il n'y ait plus de retry lors d'une erreur 500 par exemple">
<option name="closed" value="true" />
<created>1768318875533</created>
<option name="number" value="00006" />
<option name="presentableId" value="LOCAL-00006" />
<option name="project" value="LOCAL" />
<updated>1768318875533</updated>
</task>
<task id="LOCAL-00007" summary="test : ajout de TU sur les services et providers">
<option name="closed" value="true" />
<created>1768318921478</created>
<option name="number" value="00007" />
<option name="presentableId" value="LOCAL-00007" />
<option name="project" value="LOCAL" />
<updated>1768318921478</updated>
</task>
<task id="LOCAL-00008" summary="feat : ajout de la génération du bon de reception, correction de la base du formulaire multi-etape de reception et ajout d'une gestion d'erreur global">
<option name="closed" value="true" />
<created>1768498751836</created>
<option name="number" value="00008" />
<option name="presentableId" value="LOCAL-00008" />
<option name="project" value="LOCAL" />
<updated>1768498751836</updated>
</task>
<task id="LOCAL-00009" summary="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur">
<option name="closed" value="true" />
<created>1768555180530</created>
<option name="number" value="00009" />
<option name="presentableId" value="LOCAL-00009" />
<option name="project" value="LOCAL" />
<updated>1768555180530</updated>
</task>
<task id="LOCAL-00010" summary="feat : ajout de l'authentification avec lexik"> <task id="LOCAL-00010" summary="feat : ajout de l'authentification avec lexik">
<option name="closed" value="true" /> <option name="closed" value="true" />
<created>1768832208350</created> <created>1768832208350</created>
@@ -633,63 +688,7 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1770308927948</updated> <updated>1770308927948</updated>
</task> </task>
<task id="LOCAL-00052" summary="fix : logo centré en mod mobile"> <option name="localTasksCounter" value="52" />
<option name="closed" value="true" />
<created>1770310504254</created>
<option name="number" value="00052" />
<option name="presentableId" value="LOCAL-00052" />
<option name="project" value="LOCAL" />
<updated>1770310504254</updated>
</task>
<task id="LOCAL-00053" summary="feat : ajout d'un numéro de version automatique via la CI">
<option name="closed" value="true" />
<created>1770369945257</created>
<option name="number" value="00053" />
<option name="presentableId" value="LOCAL-00053" />
<option name="project" value="LOCAL" />
<updated>1770369945257</updated>
</task>
<task id="LOCAL-00054" summary="feat : update numéro de version">
<option name="closed" value="true" />
<created>1770370216428</created>
<option name="number" value="00054" />
<option name="presentableId" value="LOCAL-00054" />
<option name="project" value="LOCAL" />
<updated>1770370216428</updated>
</task>
<task id="LOCAL-00055" summary="fix : auto-tag-develop.yml">
<option name="closed" value="true" />
<created>1770370700697</created>
<option name="number" value="00055" />
<option name="presentableId" value="LOCAL-00055" />
<option name="project" value="LOCAL" />
<updated>1770370700698</updated>
</task>
<task id="LOCAL-00056" summary="fix : auto-tag-develop.yml">
<option name="closed" value="true" />
<created>1770370919043</created>
<option name="number" value="00056" />
<option name="presentableId" value="LOCAL-00056" />
<option name="project" value="LOCAL" />
<updated>1770370919043</updated>
</task>
<task id="LOCAL-00057" summary="feat : test auto-tag-develop.yml (auto incrément version)">
<option name="closed" value="true" />
<created>1770371073055</created>
<option name="number" value="00057" />
<option name="presentableId" value="LOCAL-00057" />
<option name="project" value="LOCAL" />
<updated>1770371073055</updated>
</task>
<task id="LOCAL-00058" summary="fix : nom page fournisseur">
<option name="closed" value="true" />
<created>1770632525875</created>
<option name="number" value="00058" />
<option name="presentableId" value="LOCAL-00058" />
<option name="project" value="LOCAL" />
<updated>1770632525875</updated>
</task>
<option name="localTasksCounter" value="59" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -739,6 +738,12 @@
</option> </option>
</component> </component>
<component name="VcsManagerConfiguration"> <component name="VcsManagerConfiguration">
<MESSAGE value="feat : Ajout du bundle Monolog pour la gestion des logs" />
<MESSAGE value="fix : affiche plus détail dans les logs en recette/prod" />
<MESSAGE value="fix : modification du script de déploiement pour corriger le problème d'écriture des logs de prod" />
<MESSAGE value="fix : doc de déploiement" />
<MESSAGE value="fix : doc et script de déploiement" />
<MESSAGE value="fix : gitea workflow" />
<MESSAGE value="fix : script de déploiement" /> <MESSAGE value="fix : script de déploiement" />
<MESSAGE value="feat : ajout plus d'information sur la liste des réceptions côté front sur la page d'accueil" /> <MESSAGE value="feat : ajout plus d'information sur la liste des réceptions côté front sur la page d'accueil" />
<MESSAGE value="fix : redirige sur le login sur une 401 et reset du auth state + doc + timeout du toaster" /> <MESSAGE value="fix : redirige sur le login sur une 401 et reset du auth state + doc + timeout du toaster" />
@@ -758,13 +763,7 @@
<MESSAGE value="feat : mise à jour du bon de réception" /> <MESSAGE value="feat : mise à jour du bon de réception" />
<MESSAGE value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" /> <MESSAGE value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
<MESSAGE value="feat : ajout du responsive sur la navbar et la page d'accueil" /> <MESSAGE value="feat : ajout du responsive sur la navbar et la page d'accueil" />
<MESSAGE value="fix : logo centré en mod mobile" /> <option name="LAST_COMMIT_MESSAGE" value="feat : ajout du responsive sur la navbar et la page d'accueil" />
<MESSAGE value="feat : ajout d'un numéro de version automatique via la CI" />
<MESSAGE value="feat : update numéro de version" />
<MESSAGE value="fix : auto-tag-develop.yml" />
<MESSAGE value="feat : test auto-tag-develop.yml (auto incrément version)" />
<MESSAGE value="fix : nom page fournisseur" />
<option name="LAST_COMMIT_MESSAGE" value="fix : nom page fournisseur" />
</component> </component>
<component name="XSLT-Support.FileAssociations.UIState"> <component name="XSLT-Support.FileAssociations.UIState">
<expand /> <expand />
@@ -778,4 +777,4 @@
<option value=".github/prompts" /> <option value=".github/prompts" />
</promptFileLocations> </promptFileLocations>
</component> </component>
</project> </project>

View File

@@ -29,9 +29,6 @@ Ajouter dans le fichier .env du frontend
* Finalisation de la partie réception de marchandise * Finalisation de la partie réception de marchandise
* [#267] Lister les réceptions en attente * [#267] Lister les réceptions en attente
* [#268] Lister les réceptions terminées * [#268] Lister les réceptions terminées
* [#316] Admin liste des transporteurs
* [#312] Creation administration listing fournisseurs
* [#315] Creation page admin utilisateur
### Changed ### Changed

View File

@@ -53,8 +53,6 @@ security:
- { path: ^/api/users, roles: PUBLIC_ACCESS, methods: [GET] } - { path: ^/api/users, roles: PUBLIC_ACCESS, methods: [GET] }
# Doc API (swagger) en public # Doc API (swagger) en public
- { path: ^/api/docs, roles: PUBLIC_ACCESS } - { path: ^/api/docs, roles: PUBLIC_ACCESS }
# Version de l'application en public
- { path: ^/api/version, roles: PUBLIC_ACCESS, methods: [GET] }
# Tout le reste nécessite un JWT # Tout le reste nécessite un JWT
- { path: ^/, roles: IS_AUTHENTICATED_FULLY } - { path: ^/, roles: IS_AUTHENTICATED_FULLY }

View File

@@ -8,9 +8,6 @@
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters: parameters:
imports:
- { resource: version.yaml }
services: services:
# default configuration for services in *this* file # default configuration for services in *this* file
_defaults: _defaults:

View File

@@ -1,2 +0,0 @@
parameters:
app.version: '0.0.35'

View File

@@ -3,11 +3,3 @@
<NuxtPage /> <NuxtPage />
</NuxtLayout> </NuxtLayout>
</template> </template>
<script setup lang="ts">
const { load } = useAppVersion()
onMounted(() => {
load()
})
</script>

View File

@@ -1,123 +0,0 @@
<template>
<form @submit.prevent="validate">
<div
class="flex items-center justify-between gap-10">
<h1 class="text-3xl font-bold uppercase">
{{ userId ? "Modifications de l'utilisateur" : "Ajout d'un utilisateur" }}
</h1>
<button
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
type="submit"
>
{{ userId ? 'Sauvegarder' : 'Ajouter' }}
</button>
</div>
<div class="grid gap-y-16 gap-x-40 mb-16">
<UiTextInput
id="user-name"
v-model="form.username"
label="Nom de l'utilisateur"
/>
<UiSelect
id="user-role"
v-model="form.role"
label="Rôle de l'utilisateur"
:options="ROLE"
/>
<UiTextInput
id="user-password"
v-model="form.password"
label="Mot de passe"
type="password"
/>
</div>
</form>
</template>
<script setup lang="ts">
import {computed, reactive, ref, watch} from 'vue'
import {ROLE} from '~/utils/constants'
import {createUser, updateUser, getUser} from '~/services/auth'
import type {UserData, UserFormData} from '~/services/dto/user-data'
const route = useRoute()
const router = useRouter()
const userId = computed(() => resolveUserId(route.params.id))
const isLoading = ref(false)
const isHydrating = ref(false)
const resolveUserId = (param: unknown) => {
const idStr = Array.isArray(param) ? param[0] : param
if (!idStr) {
return null
}
const id = Number(idStr)
return Number.isFinite(id) ? id : null
}
const form = reactive<UserFormData>({
username: '',
password: '',
role: ''
})
const hydrateFromUser = (user: UserData | null) => {
if (!user) {
return
}
isHydrating.value = true
form.username = user.username ?? ''
const roles = user.roles ?? []
const hasAdmin = roles.includes("ROLE_ADMIN")
form.role = hasAdmin ? "ROLE_ADMIN" : "ROLE_USER"
form.password = ''
isHydrating.value = false
}
watch(
() => userId.value,
async (id) => {
if (id === null) {
return
}
isLoading.value = true
try {
const user = await getUser(id)
hydrateFromUser(user)
} finally {
isLoading.value = false
}
},
{immediate: true}
)
async function validate() {
const normalizedUsername = form.username.trim()
const normalizedRole = form.role.trim()
const normalizedPassword = form.password.trim()
const basePayload = {
username: normalizedUsername,
roles: normalizedRole ? [normalizedRole] : undefined,
password: normalizedPassword || undefined
}
if (userId.value) {
await updateUser(userId.value, basePayload)
await router.push(`/admin/user/list/`)
return
}
const created = await createUser(basePayload)
if (created) {
await router.push(`/admin/user/list/`)
}
}
</script>

View File

@@ -1,17 +0,0 @@
export const useAppVersion = () => {
const api = useApi()
const version = useState<string | null>('app-version', () => null)
const load = async () => {
if (version.value) {
return version.value
}
const response = await api.get<{ version: string }>('version', {}, {
toast: false
})
version.value = response.version
return version.value
}
return { version, load }
}

View File

@@ -57,9 +57,7 @@
"auth": { "auth": {
"login": "Identifiants invalides.", "login": "Identifiants invalides.",
"users": "Impossible de récupérer les utilisateurs.", "users": "Impossible de récupérer les utilisateurs.",
"logout": "Impossible de se déconnecter.", "logout": "Impossible de se déconnecter."
"update": "Impossible de mettre à jour l'utilisateur.",
"create": "Impossible de créer l'utilisateur."
} }
}, },
"success": { "success": {
@@ -67,8 +65,6 @@
"update": "Réception mise à jour avec succès." "update": "Réception mise à jour avec succès."
}, },
"auth": { "auth": {
"update": "Utilisateur mis à jour avec succès.",
"create": "Utilisateur créé avec succès.",
"login": "Connexion réussie.", "login": "Connexion réussie.",
"logout": "Déconnexion réussie." "logout": "Déconnexion réussie."
} }

View File

@@ -21,25 +21,15 @@
</header> </header>
<div class="grid grid-cols-[16rem,1fr] h-[calc(100vh-85px)] min-h-0"> <div class="grid grid-cols-[16rem,1fr] h-[calc(100vh-85px)] min-h-0">
<aside class="bg-primary-500 text-white min-h-0 flex flex-col justify-between"> <aside class="bg-primary-500 text-white min-h-0 grid grid-rows-[auto,1fr,auto]">
<div class="flex flex-col gap-4 p-4 font-bold text-xl"> <div class="p-4 font-semibold">Tableau de bord</div>
<div class="overflow-y-auto min-h-0 p-4 space-y-3">
<!-- Liste des liens à ajouter ci-dessous --> <!-- Liste des liens à ajouter ci-dessous -->
<NuxtLink to="/admin/dashboard">
Tableau de bord
</NuxtLink>
<NuxtLink to="/admin/supplier/supplier-list">
Fournisseur
</NuxtLink>
<NuxtLink to="/admin/carrier/carrier-list">
Transporteur
</NuxtLink>
<NuxtLink to="/admin/user/list">
Utilisateurs
</NuxtLink>
</div> </div>
<div class="p-4"> <div class="p-4">
<p class="font-bold text-white text-left">v{{ version }}</p>
<button <button
@click="handleLogout" @click="handleLogout"
class="w-full bg-red-600 hover:bg-red-700 py-2 rounded font-bold" class="w-full bg-red-600 hover:bg-red-700 py-2 rounded font-bold"
@@ -60,10 +50,7 @@
<script setup lang="ts"> <script setup lang="ts">
import {useAuthStore} from '~/stores/auth'
const auth = useAuthStore()
const { version } = useAppVersion()
const handleLogout = async () => { const handleLogout = async () => {
try { try {
await auth.logout() await auth.logout()

View File

@@ -20,13 +20,11 @@
Accueil Accueil
</a> </a>
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink to="/admin/dashboard" custom v-slot="{ href, navigate, isActive }">
to="/admin/dashboard" custom v-slot="{ href, navigate, isActive }"
v-if="auth.isAdmin"
>
<a <a
:href="href" :href="href"
@click="navigate" @click="navigate"
:class="isReceptionActive ? 'opacity-100' : 'opacity-50'"
> >
Admin Admin
</a> </a>
@@ -103,34 +101,31 @@
<main class="mx-auto w-full max-w-[1280px] pb-0"> <main class="mx-auto w-full max-w-[1280px] pb-0">
<slot/> <slot/>
</main> </main>
<footer class="w-full mt-8 bg-primary-500 p-6">
<p class="font-bold text-white text-right">v{{ version }}</p>
</footer>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {useAuthStore} from '~/stores/auth' import { useAuthStore } from '~/stores/auth'
const route = useRoute() const route = useRoute()
const auth = useAuthStore() const auth = useAuthStore()
const isReceptionActive = computed(() => route.path.startsWith('/reception'))
const isMenuOpen = ref(false) const isMenuOpen = ref(false)
const {version} = useAppVersion()
const closeMenu = () => { const closeMenu = () => {
isMenuOpen.value = false isMenuOpen.value = false
} }
const toggleMenu = () => { const toggleMenu = () => {
isMenuOpen.value = !isMenuOpen.value isMenuOpen.value = !isMenuOpen.value
} }
const handleLogout = async () => { const handleLogout = async () => {
try { try {
await auth.logout() await auth.logout()
} finally { } finally {
closeMenu() closeMenu()
await navigateTo('/login') await navigateTo('/login')
} }
} }
</script> </script>

View File

@@ -1,51 +0,0 @@
<template>
<div class="flex items-center justify-between ">
<h1 class="text-3xl font-bold uppercase">listes des transporteurs</h1>
<button
@Click="goToCarrier()"
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] "
>Ajouter
</button>
</div>
<div class="mt-6 border border-slate-200 mb-16 ">
<div class="grid grid-cols-2 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
<div>Label</div>
<div>Code</div>
</div>
<div
v-for="carrier in carrierList"
:key="carrier.id"
class="grid grid-cols-2 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
role="button"
tabindex="0"
@click="goToCarrier(carrier.id)"
@keydown.enter="goToCarrier(carrier.id)"
>
<div>{{ carrier.name}}</div>
<div>{{ carrier.code }}</div>
</div>
</div>
</template>
<script setup lang="ts">
import type {CarrierData} from "~/services/dto/carrier-data";
import {getCarrierList} from "~/services/carrier";
const carrierList = ref<CarrierData[]>()
const router = useRouter()
const goToCarrier = (id: number|null = null) => {
id !== null ? router.push(`/admin/carrier/${id}`) : router.push(`/admin/carrier`)
}
definePageMeta({
layout: 'admin'
})
onMounted(async () => {
carrierList.value = await getCarrierList(false)
})
</script>

View File

@@ -1,9 +1,13 @@
<template>
<AdminUserForm/>
</template>
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({ definePageMeta({
layout: 'admin' layout: 'admin'
}) })
</script> </script>
<template>
<h1>test</h1>
</template>
<style scoped>
</style>

View File

@@ -1,74 +0,0 @@
<template>
<div class="flex items-center justify-between">
<h1 class="text-3xl font-bold uppercase"> Fournisseurs </h1>
<NuxtLink to="/admin/supplier"
class="flex items-center justify-center text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
>
Ajouter
</NuxtLink>
</div>
<div class="mt-6 border border-slate-200 mb-16">
<div class="max-h-96 overflow-y-auto">
<div
class="sticky top-0 z-10 grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
>
<div>Nom</div>
<div>Mail</div>
<div>Rue</div>
<div>Complément</div>
<div>Code Postal</div>
<div>Ville</div>
</div>
<div v-for="supplier in supplierList" :key="supplier.id">
<template v-if="supplier.addresses?.length">
<div
v-for="addr in supplier.addresses"
:key="addr.id"
class="grid grid-cols-6 hover:bg-slate-50 border-t gap-4 px-4 py-2"
@click="goToSupplier(supplier.id)"
>
<div class="truncate">
{{ supplier.name }}
</div>
<div class="truncate">
{{ supplier.email }}
</div>
<div class="truncate">
{{ addr.street }}
</div>
<div class="truncate">
{{ addr.street2 }}
</div>
<div>{{ addr.postalCode }}</div>
<div class="uppercase truncate">
{{ addr.city }}
</div>
</div>
</template>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type {SupplierData} from "~/services/dto/supplier-data"
import {getSupplierList} from "~/services/supplier"
definePageMeta({layout: "admin"})
const supplierList = ref<SupplierData[]>([])
const router = useRouter()
const goToSupplier = (id: number) => {
router.push(`/admin/supplier/${id}`)
}
onMounted(async () => {
supplierList.value = (await getSupplierList(false)) ?? []
})
</script>

View File

@@ -1,8 +0,0 @@
<template>
<UserForm/>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'admin'
})
</script>

View File

@@ -1,57 +0,0 @@
<template>
<div class="flex items-center justify-between">
<h1 class="text-3xl font-bold uppercase">Liste des utilisateurs</h1>
<NuxtLink
class="flex items-center justify-center text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
@click="router.push('/admin/user/')"
>
Ajouter
</NuxtLink>
</div>
<div>
<div class="mt-6 border border-slate-200 mb-16 ">
<div class="grid grid-cols-3 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
<div>Username</div>
<div>Role</div>
</div>
<div
v-for="user in userList"
:key="user.id"
class="grid grid-cols-3 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t items-center"
role="button"
tabindex="0"
@click="goToUser(user.id)"
>
<div>
{{ user.username }}
</div>
<div>
{{ user.roles?.join(', ') || ' ---' }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'admin'
})
import type {UserData} from "~/services/dto/user-data";
import {getAdminUsers, getUsers} from "~/services/auth";
const userList = ref<UserData[]>([])
const router = useRouter()
const goToUser = (id: number) => {
router.push(`/admin/user/${id}`)
}
onMounted(async () => {
userList.value = await getAdminUsers()
})
</script>

View File

@@ -46,8 +46,7 @@
> >
Connexion Connexion
</button> </button>
<p class="font-bold">v{{ version }}</p> </form>
</form>
</div> </div>
</template> </template>
@@ -58,7 +57,6 @@ import { useAuthStore } from '~/stores/auth'
const router = useRouter() const router = useRouter()
const auth = useAuthStore() const auth = useAuthStore()
const { version } = useAppVersion()
definePageMeta({ definePageMeta({
layout: 'auth' layout: 'auth'

View File

@@ -1,6 +1,5 @@
import { useApi } from '~/composables/useApi' import { useApi } from '~/composables/useApi'
import type { UserData } from '~/services/dto/user-data' import type { UserData } from '~/services/dto/user-data'
import type {UserPayload} from "~/services/dto/user-data";
export async function getUsers() { export async function getUsers() {
const api = useApi() const api = useApi()
@@ -13,40 +12,7 @@ export async function getUsers() {
return data['hydra:member'] ?? [] return data['hydra:member'] ?? []
} }
export async function getAdminUsers() {
const api = useApi()
const data = await api.get<UserData[] | { 'hydra:member': UserData[] }>('admin/users', {}, {
toastErrorKey: 'errors.auth.users'
})
if (Array.isArray(data)) {
return data
}
return data['hydra:member'] ?? []
}
export async function getUser(id: number) {
const api = useApi()
return api.get<UserData>(`users/${id}`, {}, {
toastErrorKey: 'errors.auth.user'
})
}
export async function createUser(payload: UserPayload = {}) {
const api = useApi()
return api.post<UserData>('users', payload, {
toastErrorKey: 'errors.auth.create',
toastSuccessKey : 'success.auth.create'
})
}
export async function updateUser(id : number, playload: UserPayload = {}){
const api = useApi()
return api.patch<UserData>(`users/${id}`, playload, {
toastErrorKey: 'errors.auth.update',
toastSuccessKey: 'success.auth.update'
})
}
export async function getCurrentUser() { export async function getCurrentUser() {
const api = useApi() const api = useApi()
return api.get<UserData>('me', {}, { return api.get<UserData>('me', {}, {

View File

@@ -1,17 +1,4 @@
export interface UserData { export interface UserData {
id: number id: number
username: string username: string
roles: string[]
}
export type UserPayload = {
username?: string
password?: string
roles?: string[]
}
export type UserFormData = {
username: string
password: string
role: string
} }

View File

@@ -1,80 +1,63 @@
import {defineStore} from 'pinia' import { defineStore } from 'pinia'
import type {UserData} from '~/services/dto/user-data' import type { UserData } from '~/services/dto/user-data'
import {getCurrentUser, createUser, login, logout} from '~/services/auth' import { getCurrentUser, login, logout } from '~/services/auth'
import type {UserPayload} from "~/services/dto/user-data";
import {ROLE} from '~/utils/constants'
export const useAuthStore = defineStore('auth', { export const useAuthStore = defineStore('auth', {
state: () => ({ state: () => ({
user: null as UserData | null, user: null as UserData | null,
isLoading: false, isLoading: false,
checked: false checked: false
}), }),
getters: { getters: {
isAuthenticated: (state) => Boolean(state.user), isAuthenticated: (state) => Boolean(state.user)
isAdmin: (state) => Boolean(state.user?.roles?.includes(ROLE[0].value)) },
actions: {
clearSession() {
this.user = null
this.checked = true
this.isLoading = false
}, },
actions: { async ensureSession() {
clearSession() { if (this.checked) {
this.user = null return this.user
this.checked = true }
this.isLoading = false
},
async ensureSession() {
if (this.checked) {
return this.user
}
this.checked = true this.checked = true
try { try {
const me = await getCurrentUser() const me = await getCurrentUser()
this.user = me this.user = me
return me return me
} catch { } catch {
this.user = null this.user = null
return null return null
} }
}, },
async login(username: string, password: string) { async login(username: string, password: string) {
this.isLoading = true this.isLoading = true
try { try {
await login(username, password) await login(username, password)
const me = await getCurrentUser() const me = await getCurrentUser()
this.user = me this.user = me
this.checked = true this.checked = true
return me return me
} finally { } finally {
this.isLoading = false this.isLoading = false
} }
}, },
async createUser(payload: UserPayload = {}) { async logout() {
this.isLoading = true this.isLoading = true
const result = await createUser(payload).finally(() => {
this.isLoading = false
})
return result
},
async updateUser(id: number, payload: UserPayload) {
this.isLoading = true
const result = await createUser(payload).finally(() => {
this.isLoading = false
})
return result
},
async logout() {
this.isLoading = true
try { try {
await logout() await logout()
} catch { } catch {
// Ignore logout errors so we can still clear local auth state. // Ignore logout errors so we can still clear local auth state.
} finally { } finally {
this.user = null this.user = null
this.checked = true this.checked = true
this.isLoading = false this.isLoading = false
} }
},
} }
}
}) })

View File

@@ -8,10 +8,6 @@ export const MERCHANDISE_TYPE_CODES = {
AUTRES: 'AUTRES' AUTRES: 'AUTRES'
} as const } as const
export const ROLE = [
{ label: 'Administrateur', value: 'ROLE_ADMIN' },
{ label: 'Utilisateur', value: 'ROLE_USER' }
]
export const SUPLLIER_CODE = { export const SUPLLIER_CODE = {
LIOT: 'LIOT' LIOT: 'LIOT'
} }

View File

@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace App\ApiResource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use App\State\AppVersionProvider;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new Get(
uriTemplate: '/version',
normalizationContext: ['groups' => ['version:read']],
provider: AppVersionProvider::class,
),
],
)]
final class AppVersion
{
#[Groups(['version:read'])]
public string $version = '';
}

0
src/Entity/.gitignore vendored Normal file
View File

View File

@@ -7,10 +7,7 @@ namespace App\Entity;
use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\State\MeProvider; use App\State\MeProvider;
use App\State\UserPasswordProcessor;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
@@ -31,27 +28,10 @@ use Symfony\Component\Serializer\Attribute\Groups;
normalizationContext: ['groups' => ['user:read']], normalizationContext: ['groups' => ['user:read']],
security: "is_granted('ROLE_USER')" security: "is_granted('ROLE_USER')"
), ),
new Post(
normalizationContext: ['groups' => ['user:read']],
denormalizationContext: ['groups' => ['user:write']],
security: "is_granted('ROLE_ADMIN')",
processor: UserPasswordProcessor::class
),
new Patch(
normalizationContext: ['groups' => ['user:read']],
denormalizationContext: ['groups' => ['user:write']],
security: "is_granted('ROLE_ADMIN')",
processor: UserPasswordProcessor::class
),
new GetCollection( new GetCollection(
normalizationContext: ['groups' => ['user-login:read']], normalizationContext: ['groups' => ['user:read']],
security: "is_granted('PUBLIC_ACCESS')" security: "is_granted('PUBLIC_ACCESS')"
), ),
new GetCollection(
uriTemplate: '/admin/users',
normalizationContext: ['groups' => ['user:read']],
security: "is_granted('ROLE_ADMIN')"
),
], ],
normalizationContext: ['groups' => ['user:read']], normalizationContext: ['groups' => ['user:read']],
paginationEnabled: false paginationEnabled: false
@@ -61,19 +41,17 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')] #[ORM\Column(type: 'integer')]
#[Groups(['user:read', 'user-login:read', 'reception:read'])] #[Groups(['user:read', 'reception:read'])]
private ?int $id = null; private ?int $id = null;
#[ORM\Column(length: 180, unique: true)] #[ORM\Column(length: 180, unique: true)]
#[Groups(['user:read', 'user:write', 'user-login:read', 'reception:read'])] #[Groups(['user:read', 'reception:read'])]
private string $username = ''; private string $username = '';
#[ORM\Column(type: 'json')] #[ORM\Column(type: 'json')]
#[Groups(['user:write', 'user:read'])]
private array $roles = []; private array $roles = [];
#[ORM\Column] #[ORM\Column]
#[Groups(['user:write'])]
private string $password = ''; private string $password = '';
public function getId(): ?int public function getId(): ?int

View File

@@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\ApiResource\AppVersion;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final readonly class AppVersionProvider implements ProviderInterface
{
public function __construct(
#[Autowire('%app.version%')]
private string $version,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): AppVersion
{
$dto = new AppVersion();
$dto->version = $this->version;
return $dto;
}
}

View File

@@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\User;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
final class UserPasswordProcessor implements ProcessorInterface
{
public function __construct(
private readonly UserPasswordHasherInterface $hasher,
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
private readonly ProcessorInterface $persistProcessor
) {}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
{
if ($data instanceof User) {
$plain = $data->getPassword();
if ('' !== $plain) {
$data->setPassword($this->hasher->hashPassword(
$data,
$plain
));
}
}
return $this->persistProcessor->process(
$data,
$operation,
$uriVariables,
$context
);
}
}