Migrate grateWizard from React/Next.js to native Nuxt integration
- Port all React components to Vue 3 (GwTabs, GwMN, GwCRA, GwCRS, GwMap, GwRelations, GwPerimeterList) - Port hooks to Vue composables (useCesiumProfiles, useSavedPerimeters) - Copy pure TS services and utils (duniter/, ss58, gratewizard utils) - Add Leaflet + Geoman + MarkerCluster dependencies - Serve grateWizard as popup via /gratewizard?popup (layout: false) and info page on /gratewizard (with Librodrome layout) - Remove public/gratewizard-app/ static Next.js export - Refine UI: compact tabs, buttons, inputs, cards, perimeter list - Use Ğ1 breve everywhere, French locale for all dates and amounts - Rename roles: vendeur→offre / acheteur→reçoit le produit ou service - Rename prix→évaluation in all visible text - Add calculated result column in CRA and CRS relation tables - DU/Ğ1 selector uses toggle switch (same as role toggle) - Auto-scroll to monetary data card on polygon selection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
194
app/components/gratewizard/GwCRA.vue
Normal file
194
app/components/gratewizard/GwCRA.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-5 pt-4">
|
||||
<div class="card-surface">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<p class="gw-metric">Coefficient relatif à l'ancienneté</p>
|
||||
|
||||
<!-- My seniority -->
|
||||
<div class="flex flex-col items-center gap-1 w-full max-w-80">
|
||||
<label class="gw-text text-sm">Mon ancienneté</label>
|
||||
<input
|
||||
v-model="myDate"
|
||||
type="date"
|
||||
:min="Block0Date"
|
||||
:max="todayDate"
|
||||
class="gw-input w-full"
|
||||
/>
|
||||
<p :class="['gw-text text-center text-sm', { invisible: !myDate }]">
|
||||
{{ getDays(myDate) || 0 }} DUs créés
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Role -->
|
||||
<div class="flex items-center justify-center gap-2 mt-2">
|
||||
<span class="gw-text text-xs text-right">offre<br />le produit ou service</span>
|
||||
<label class="gw-toggle shrink-0">
|
||||
<input v-model="isSeller" type="checkbox" />
|
||||
<span class="gw-toggle-slider" />
|
||||
</label>
|
||||
<span class="gw-text text-xs">reçoit<br />le produit ou service</span>
|
||||
</div>
|
||||
|
||||
<!-- Other party name + add button -->
|
||||
<div class="flex items-center gap-4 w-full max-w-80">
|
||||
<div class="flex-1">
|
||||
<label class="gw-text text-sm">Nom {{ !isSeller ? 'du receveur' : "de l'offreur" }}</label>
|
||||
<input
|
||||
v-model="otherName"
|
||||
type="text"
|
||||
maxlength="25"
|
||||
class="gw-input w-full"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="!isBaseFriend"
|
||||
class="gw-icon-btn self-end mb-0.5"
|
||||
:disabled="!otherName || !otherDate || isFriend"
|
||||
:title="friends.some((f) => f.name === otherName) ? 'Mettre \u00e0 jour' : 'Ajouter relation'"
|
||||
@click="addFriend"
|
||||
>
|
||||
<svg v-if="friends.some((f) => f.name === otherName)" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16" /></svg>
|
||||
<svg v-else class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Other party date -->
|
||||
<div class="flex flex-col items-center gap-1 w-full max-w-80">
|
||||
<label class="gw-text text-sm">
|
||||
Ancienneté {{ otherName ? 'de ' + otherName : !isSeller ? 'du receveur' : "de l'offreur" }}
|
||||
</label>
|
||||
<input
|
||||
v-model="otherDate"
|
||||
type="date"
|
||||
:min="Block0Date"
|
||||
:max="todayDate"
|
||||
class="gw-input w-full max-w-64"
|
||||
/>
|
||||
<p :class="['gw-text text-center text-sm', { invisible: !otherDate }]">
|
||||
{{ getDays(otherDate) || 0 }} DUs créés
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Price -->
|
||||
<div class="flex items-center gap-3 justify-center">
|
||||
<label class="gw-text text-sm whitespace-nowrap">Évaluation de réf.</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
:value="price"
|
||||
type="number"
|
||||
min="0"
|
||||
:step="Math.ceil(Number(price) / 10 / 2) || 1"
|
||||
placeholder="0.00"
|
||||
class="gw-input w-40 pr-14"
|
||||
@input="price = Math.min(Number(($event.target as HTMLInputElement).value), 9999).toString()"
|
||||
/>
|
||||
<select
|
||||
v-model="currency"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 bg-transparent text-white/40 text-sm cursor-pointer border-0 outline-none"
|
||||
@change="price = '1'"
|
||||
>
|
||||
<option value="DU" class="bg-surface-100">DU</option>
|
||||
<option value="G1" class="bg-surface-100">Ğ1</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Discount -->
|
||||
<div class="flex items-center gap-3 justify-center">
|
||||
<label class="gw-text text-sm whitespace-nowrap">Réduction newbie</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
:value="discount"
|
||||
type="number"
|
||||
min="0"
|
||||
:step="Math.ceil(Number(discount) / 10 / 2) || 1"
|
||||
placeholder="0"
|
||||
class="gw-input w-40 pr-8"
|
||||
@input="discount = Math.min(Number(($event.target as HTMLInputElement).value), 99).toString()"
|
||||
/>
|
||||
<span class="absolute right-2 top-1/2 -translate-y-1/2 text-white/40 text-sm pointer-events-none">%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Corrected price -->
|
||||
<p class="gw-title mt-4">
|
||||
Évaluation corrigée : {{ fr(getFinalPrice(isSeller ? myDate : otherDate)) }} {{ currencyDisplay }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Relations table -->
|
||||
<GratewizardGwRelations
|
||||
:items="tableItems"
|
||||
:base-friends="baseFriends"
|
||||
result-label="ÉVAL."
|
||||
@select="(f) => { otherName = f.name; otherDate = f.date; }"
|
||||
@remove="removeFriend"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Block0Date, getDays, getRatio, dateToString, fr, type Friend, type TableFriend } from '~/utils/gratewizard';
|
||||
|
||||
const todayDate = dateToString(new Date());
|
||||
const baseFriends: Friend[] = [
|
||||
{ name: 'Bloc 0', date: Block0Date },
|
||||
{ name: 'Newbie', date: todayDate },
|
||||
];
|
||||
|
||||
const price = useLocalStorage('price', '1');
|
||||
const discount = useLocalStorage('discount', '0');
|
||||
const myDate = useLocalStorage<string | undefined>('myDate', undefined);
|
||||
const isSeller = useLocalStorage('isSeller', true);
|
||||
const currency = useLocalStorage('currency', 'DU');
|
||||
const otherName = useLocalStorage('otherName', '');
|
||||
const otherDate = useLocalStorage<string | undefined>('otherDate', undefined);
|
||||
const friends = useLocalStorage<Friend[]>('friends', []);
|
||||
|
||||
const currencyDisplay = computed(() => currency.value === 'G1' ? '\u011e1' : 'DU');
|
||||
|
||||
function getFinalPrice(date: string | undefined): number {
|
||||
if ((!date && !myDate.value) || (!date && !otherDate.value)) return Number(price.value);
|
||||
const ratio = date
|
||||
? myDate.value && (!isSeller.value || !otherDate.value)
|
||||
? getRatio(date, myDate.value)
|
||||
: getRatio(date, otherDate.value)
|
||||
: 1;
|
||||
const d = Number(discount.value) / 100;
|
||||
const p = Number(price.value);
|
||||
return (1 - d) * p + d * p * ratio;
|
||||
}
|
||||
|
||||
function addFriend() {
|
||||
if (!otherDate.value || !otherName.value) return;
|
||||
if (friends.value.some((f) => f.name === otherName.value)) {
|
||||
friends.value = friends.value.map((f) =>
|
||||
f.name === otherName.value ? { ...f, date: otherDate.value! } : f
|
||||
);
|
||||
} else {
|
||||
friends.value = [...friends.value, { name: otherName.value, date: otherDate.value }];
|
||||
}
|
||||
}
|
||||
|
||||
function removeFriend(name: string) {
|
||||
friends.value = friends.value.filter((f) => f.name !== name);
|
||||
}
|
||||
|
||||
const isBaseFriend = computed(() =>
|
||||
baseFriends.some((f) => f.name === otherName.value && f.date === otherDate.value)
|
||||
);
|
||||
const isFriend = computed(() =>
|
||||
friends.value.some((f) => f.name === otherName.value && f.date === otherDate.value)
|
||||
);
|
||||
|
||||
const tableItems = computed<TableFriend[]>(() => {
|
||||
return baseFriends.concat(friends.value).map((friend) => ({
|
||||
...friend,
|
||||
displayName: friend.name.substring(0, 10),
|
||||
displayDate: new Date(friend.date).toLocaleDateString('fr-FR', { dateStyle: 'short' }),
|
||||
result: fr(getFinalPrice(friend.date)),
|
||||
du: getDays(friend.date),
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user