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 }}
persist-credentials: true
- name: Create next tag from config/version.yaml
- name: Create next tag v0.0.X
shell: bash
run: |
set -euo pipefail
# Skip if current commit already has a vX.Y.Z tag
if git tag --points-at HEAD | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
# Skip if current commit already has a v0.0.* tag
if git tag --points-at HEAD | grep -qE '^v0\.0\.'; then
echo "Tag already exists on this commit. Skipping."
exit 0
fi
changed_version=false
if git diff --name-only "${{ gitea.event.before }}" "${{ gitea.event.after }}" | grep -q '^config/version\.yaml$'; then
changed_version=true
fi
read_version() {
awk -F': *' '/app\.version:/{print $2}' config/version.yaml | tr -d '[:space:]' | tr -d "'\""
}
if $changed_version; then
version="$(read_version)"
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid version in version.yaml: $version" >&2
last_tag="$(git tag -l 'v0.0.*' --sort=-v:refname | head -n1 || true)"
if [ -z "$last_tag" ]; then
next_tag="v0.0.1"
else
patch="${last_tag##v0.0.}"
if ! [[ "$patch" =~ ^[0-9]+$ ]]; then
echo "Unexpected tag format: $last_tag" >&2
exit 1
fi
else
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
next_tag="v0.0.$((patch + 1))"
fi
tag="v$version"
git tag "$tag"
git push origin "$tag"
git config user.name "gitea-actions"
git config user.email "gitea-actions@local"
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" />
</component>
<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$/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/pages/admin/supplier/supplier-list.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/admin/supplier/supplier-list.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/layouts/default.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/layouts/default.vue" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -227,7 +226,7 @@
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "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",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
@@ -252,11 +251,11 @@
}]]></component>
<component name="RecentsManager">
<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\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\Ferme_MCD\MCD_DOC" />
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\frontend\pages\reception" />
</key>
</component>
<component name="SharedIndexes">
@@ -297,6 +296,62 @@
<workItem from="1770195959162" duration="18915000" />
<workItem from="1770274844804" duration="3940000" />
</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">
<option name="closed" value="true" />
<created>1768832208350</created>
@@ -633,63 +688,7 @@
<option name="project" value="LOCAL" />
<updated>1770308927948</updated>
</task>
<task id="LOCAL-00052" summary="fix : logo centré en mod mobile">
<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" />
<option name="localTasksCounter" value="52" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -739,6 +738,12 @@
</option>
</component>
<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="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" />
@@ -758,13 +763,7 @@
<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 du responsive sur la navbar et la page d'accueil" />
<MESSAGE value="fix : logo centré en mod mobile" />
<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" />
<option name="LAST_COMMIT_MESSAGE" value="feat : ajout du responsive sur la navbar et la page d'accueil" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />
@@ -778,4 +777,4 @@
<option value=".github/prompts" />
</promptFileLocations>
</component>
</project>
</project>

View File

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

View File

@@ -53,8 +53,6 @@ security:
- { path: ^/api/users, roles: PUBLIC_ACCESS, methods: [GET] }
# Doc API (swagger) en public
- { 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
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }

View File

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

View File

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

View File

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

View File

@@ -21,25 +21,15 @@
</header>
<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">
<div class="flex flex-col gap-4 p-4 font-bold text-xl">
<aside class="bg-primary-500 text-white min-h-0 grid grid-rows-[auto,1fr,auto]">
<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 -->
<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 class="p-4">
<p class="font-bold text-white text-left">v{{ version }}</p>
<button
@click="handleLogout"
class="w-full bg-red-600 hover:bg-red-700 py-2 rounded font-bold"
@@ -60,10 +50,7 @@
<script setup lang="ts">
import {useAuthStore} from '~/stores/auth'
const auth = useAuthStore()
const { version } = useAppVersion()
const handleLogout = async () => {
try {
await auth.logout()

View File

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

View File

@@ -1,6 +1,5 @@
import { useApi } from '~/composables/useApi'
import type { UserData } from '~/services/dto/user-data'
import type {UserPayload} from "~/services/dto/user-data";
export async function getUsers() {
const api = useApi()
@@ -13,40 +12,7 @@ export async function getUsers() {
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() {
const api = useApi()
return api.get<UserData>('me', {}, {

View File

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

View File

@@ -8,10 +8,6 @@ export const MERCHANDISE_TYPE_CODES = {
AUTRES: 'AUTRES'
} as const
export const ROLE = [
{ label: 'Administrateur', value: 'ROLE_ADMIN' },
{ label: 'Utilisateur', value: 'ROLE_USER' }
]
export const SUPLLIER_CODE = {
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\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\State\MeProvider;
use App\State\UserPasswordProcessor;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
@@ -31,27 +28,10 @@ use Symfony\Component\Serializer\Attribute\Groups;
normalizationContext: ['groups' => ['user:read']],
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(
normalizationContext: ['groups' => ['user-login:read']],
normalizationContext: ['groups' => ['user:read']],
security: "is_granted('PUBLIC_ACCESS')"
),
new GetCollection(
uriTemplate: '/admin/users',
normalizationContext: ['groups' => ['user:read']],
security: "is_granted('ROLE_ADMIN')"
),
],
normalizationContext: ['groups' => ['user:read']],
paginationEnabled: false
@@ -61,19 +41,17 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
#[Groups(['user:read', 'user-login:read', 'reception:read'])]
#[Groups(['user:read', 'reception:read'])]
private ?int $id = null;
#[ORM\Column(length: 180, unique: true)]
#[Groups(['user:read', 'user:write', 'user-login:read', 'reception:read'])]
#[Groups(['user:read', 'reception:read'])]
private string $username = '';
#[ORM\Column(type: 'json')]
#[Groups(['user:write', 'user:read'])]
private array $roles = [];
#[ORM\Column]
#[Groups(['user:write'])]
private string $password = '';
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
);
}
}