- 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>
209 lines
8.0 KiB
Vue
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ç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é {{ 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éé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é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">Ğ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 × (1 + w × (1 − S / C))
|
|
</p>
|
|
<template v-if="seniority > 0">
|
|
<p class="gw-text text-xs font-mono mb-2">
|
|
{{ fr(v) }} × (1 + {{ fr(w) }} × (1 − {{ fr(s, 0) }} / {{ fr(seniority, 0) }}))
|
|
= {{ fr(v) }} × {{ fr(factor) }}
|
|
</p>
|
|
<p class="gw-title">= {{ fr(correctedValue) }} {{ currencyDisplay }}</p>
|
|
</template>
|
|
<p v-else class="gw-text text-xs">Renseigner l'ancienneté 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>
|