Files
librodrome/app/components/gratewizard/GwCRS.vue
Yvv 2b5543791f 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>
2026-02-25 16:05:43 +01:00

209 lines
8.0 KiB
Vue

<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 au solde net</p>
<!-- Buyer 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">Qui re&ccedil;oit le produit ou service ?</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>
<!-- Buyer seniority -->
<div class="flex flex-col items-center gap-1 w-full max-w-80">
<label class="gw-text text-sm">
Anciennet&eacute; {{ otherName ? 'de ' + otherName : 'du receveur' }}
</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 }]">
{{ seniority || 0 }} DUs cr&eacute;&eacute;s
</p>
</div>
<!-- DU Balance -->
<div class="flex items-center gap-3 justify-center">
<label class="gw-text text-sm whitespace-nowrap">Solde DU{{ otherName ? ' de ' + otherName : ' du receveur' }}</label>
<div class="relative">
<input
:value="balance"
type="number"
min="0"
placeholder="0"
class="gw-input w-40 pr-10"
@input="balance = Math.max(0, Number(($event.target as HTMLInputElement).value)).toString()"
/>
<span class="absolute right-2 top-1/2 -translate-y-1/2 text-white/40 text-sm pointer-events-none">DU</span>
</div>
</div>
<!-- Net balance -->
<p v-if="seniority > 0" class="gw-text text-center">
Solde net : {{ fr(netBalance, 0) }} DU
{{ netBalance < 0
? ' (b\u00e9n\u00e9ficie de valeurs \u2192 majoration)'
: netBalance > 0
? ' (alimente en valeurs \u2192 minoration)'
: ' (\u00e9quilibr\u00e9)' }}
</p>
<!-- Reference value -->
<div class="flex items-center gap-3 justify-center">
<label class="gw-text text-sm whitespace-nowrap">Valeur de r&eacute;f.</label>
<div class="relative">
<input
:value="refValue"
type="number"
min="0"
:step="Math.ceil(v / 10 / 2) || 1"
placeholder="0.00"
class="gw-input w-40 pr-14"
@input="refValue = 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="refValue = '1'"
>
<option value="DU" class="bg-surface-100">DU</option>
<option value="G1" class="bg-surface-100">&#x11E;1</option>
</select>
</div>
</div>
<!-- Weight slider -->
<div class="flex items-center gap-3 w-full max-w-80">
<span class="gw-text whitespace-nowrap text-sm">Poids</span>
<input
v-model="weight"
type="range"
min="0"
max="1"
step="0.01"
class="flex-1 h-2 rounded-lg appearance-none cursor-pointer accent-accent"
/>
<span class="gw-text text-sm w-10 text-right">{{ fr(w) }}</span>
</div>
<!-- Formula + result -->
<div class="w-full max-w-sm text-center p-4 rounded-lg bg-surface-200">
<p class="gw-text text-xs font-mono mb-1">
V &times; (1 + w &times; (1 &minus; S / C))
</p>
<template v-if="seniority > 0">
<p class="gw-text text-xs font-mono mb-2">
{{ fr(v) }} &times; (1 + {{ fr(w) }} &times; (1 &minus; {{ fr(s, 0) }} / {{ fr(seniority, 0) }}))
= {{ fr(v) }} &times; {{ fr(factor) }}
</p>
<p class="gw-title">= {{ fr(correctedValue) }} {{ currencyDisplay }}</p>
</template>
<p v-else class="gw-text text-xs">Renseigner l'anciennet&eacute; pour calculer</p>
</div>
</div>
</div>
<!-- Relations table (shared with CRA) -->
<GratewizardGwRelations
:items="tableItems"
:base-friends="baseFriends"
result-label="VALEUR"
@select="(f) => { otherName = f.name; otherDate = f.date; }"
@remove="removeFriend"
/>
</div>
</template>
<script setup lang="ts">
import { Block0Date, getDays, dateToString, fr, type Friend, type TableFriend } from '~/utils/gratewizard';
const todayDate = dateToString(new Date());
const baseFriends: Friend[] = [{ name: 'Bloc 0', date: Block0Date }];
// Shared state with CRA (same localStorage keys)
const currency = useLocalStorage('currency', 'DU');
const friends = useLocalStorage<Friend[]>('friends', []);
const otherName = useLocalStorage('otherName', '');
const otherDate = useLocalStorage<string | undefined>('otherDate', undefined);
// CRS-specific state
const refValue = useLocalStorage('crs-value', '1');
const balance = useLocalStorage('crs-balance', '0');
const weight = useLocalStorage('crs-weight', '0.50');
const currencyDisplay = computed(() => currency.value === 'G1' ? '\u011e1' : 'DU');
const seniority = computed(() => getDays(otherDate.value));
const s = computed(() => Number(balance.value));
const w = computed(() => Number(weight.value));
const v = computed(() => Number(refValue.value));
const netBalance = computed(() => s.value - seniority.value);
const correctedValue = computed(() =>
seniority.value > 0 ? v.value * (1 + w.value * (1 - s.value / seniority.value)) : v.value
);
const factor = computed(() =>
seniority.value > 0 ? 1 + w.value * (1 - s.value / seniority.value) : 1
);
// Friend management (shared with CRA)
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)
);
function getCorrectedValueFor(friendDate: string): number {
const c = getDays(friendDate);
if (c <= 0) return v.value;
return v.value * (1 + w.value * (1 - s.value / c));
}
const tableItems = computed<TableFriend[]>(() => {
return baseFriends.concat(friends.value).map((f) => ({
...f,
displayName: f.name.substring(0, 10),
displayDate: new Date(f.date).toLocaleDateString('fr-FR', { dateStyle: 'short' }),
result: fr(getCorrectedValueFor(f.date)),
du: getDays(f.date),
}));
});
</script>