- 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>
195 lines
7.5 KiB
Vue
195 lines
7.5 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 à 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>
|