Merge design_ui_v2 into design_ui so the new design system can be previewed without dropping later product work.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,8 +6,15 @@ import { formatMoney } from '@/lib/money';
|
||||
import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { canManageSettings } from '@/lib/roles';
|
||||
import UiButton from '@/components/ui/UiButton.vue';
|
||||
import UiInput from '@/components/ui/UiInput.vue';
|
||||
import {
|
||||
UiBadge,
|
||||
UiButton,
|
||||
UiCard,
|
||||
UiInput,
|
||||
UiPageHeader,
|
||||
UiSelect,
|
||||
} from '@/components/ui';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import type {
|
||||
InventorySummaryDto,
|
||||
StockMovementDto,
|
||||
@@ -103,78 +110,83 @@ onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page" dir="rtl">
|
||||
<header class="head">
|
||||
<div>
|
||||
<h1>انبار</h1>
|
||||
<p class="sub">
|
||||
موجودی کالا، ورود خرید و مانده وقتی تهحساب وصل نباشد
|
||||
</p>
|
||||
</div>
|
||||
<UiButton variant="ghost" type="button" :disabled="loading" @click="load">
|
||||
بروزرسانی
|
||||
</UiButton>
|
||||
</header>
|
||||
<section class="crm-page">
|
||||
<UiPageHeader
|
||||
title="انبار"
|
||||
lead="موجودی کالا، ورود خرید و مانده وقتی تهحساب وصل نباشد"
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton variant="ghost" type="button" :disabled="loading" @click="load">
|
||||
<UiIcon name="refresh" :size="16" />
|
||||
بروزرسانی
|
||||
</UiButton>
|
||||
</template>
|
||||
</UiPageHeader>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-if="error" class="error-msg">{{ error }}</p>
|
||||
<p v-else-if="loading && !summary" class="muted">در حال بارگذاری…</p>
|
||||
|
||||
<div v-else-if="summary && !summary.enabled" class="notice">
|
||||
<div v-else-if="summary && !summary.enabled" class="notice-box">
|
||||
اتصال تهحساب فعال است؛ موجودی و مانده از حسابداری خارجی خوانده میشود.
|
||||
</div>
|
||||
|
||||
<template v-else-if="summary">
|
||||
<div class="kpis">
|
||||
<article>
|
||||
<span>صندوق (وصولها)</span>
|
||||
<strong>{{ formatMoney(summary.cashTotal, summary.currency) }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>مانده طرفحسابها</span>
|
||||
<strong>{{
|
||||
formatMoney(summary.receivablesTotal, summary.currency)
|
||||
}}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>انبار</span>
|
||||
<strong>{{ summary.warehouseName }}</strong>
|
||||
</article>
|
||||
<div class="kpis-grid">
|
||||
<UiCard padding="sm" class="kpi-card">
|
||||
<span class="kpi-label">صندوق (وصولها)</span>
|
||||
<strong class="kpi-value">{{ formatMoney(summary.cashTotal, summary.currency) }}</strong>
|
||||
</UiCard>
|
||||
<UiCard padding="sm" class="kpi-card">
|
||||
<span class="kpi-label">مانده طرفحسابها</span>
|
||||
<strong class="kpi-value">{{ formatMoney(summary.receivablesTotal, summary.currency) }}</strong>
|
||||
</UiCard>
|
||||
<UiCard padding="sm" class="kpi-card">
|
||||
<span class="kpi-label">نام انبار</span>
|
||||
<strong class="kpi-value">{{ summary.warehouseName }}</strong>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<form v-if="canWrite" class="move-form" @submit.prevent="submitMove">
|
||||
<select v-model="moveType">
|
||||
<option value="purchase">ورود خرید</option>
|
||||
<option value="adjustment">اصلاح موجودی (عدد جدید)</option>
|
||||
</select>
|
||||
<select v-model="productId">
|
||||
<option value="" disabled>محصول</option>
|
||||
<option
|
||||
v-for="p in summary.products"
|
||||
:key="p.productId"
|
||||
:value="p.productId"
|
||||
>
|
||||
{{ p.name }}
|
||||
</option>
|
||||
</select>
|
||||
<UiInput
|
||||
v-model="quantity"
|
||||
type="number"
|
||||
:placeholder="moveType === 'adjustment' ? 'موجودی جدید' : 'تعداد'"
|
||||
/>
|
||||
<UiInput v-model="note" placeholder="توضیح (اختیاری)" />
|
||||
<UiButton type="submit" :disabled="saving || !productId">
|
||||
{{ saving ? 'ثبت…' : 'ثبت گردش' }}
|
||||
</UiButton>
|
||||
</form>
|
||||
<UiCard v-if="canWrite" padding="sm" class="move-form-card">
|
||||
<form class="move-form" @submit.prevent="submitMove">
|
||||
<UiSelect v-model="moveType">
|
||||
<option value="purchase">ورود خرید</option>
|
||||
<option value="adjustment">اصلاح موجودی (عدد جدید)</option>
|
||||
</UiSelect>
|
||||
<UiSelect v-model="productId">
|
||||
<option value="" disabled>انتخاب محصول</option>
|
||||
<option
|
||||
v-for="p in summary.products"
|
||||
:key="p.productId"
|
||||
:value="p.productId"
|
||||
>
|
||||
{{ p.name }}
|
||||
</option>
|
||||
</UiSelect>
|
||||
<UiInput
|
||||
v-model="quantity"
|
||||
type="number"
|
||||
:placeholder="moveType === 'adjustment' ? 'موجودی جدید' : 'تعداد'"
|
||||
/>
|
||||
<UiInput v-model="note" placeholder="توضیح اختیاری…" />
|
||||
<UiButton variant="primary" type="submit" :disabled="saving || !productId" :loading="saving">
|
||||
ثبت گردش
|
||||
</UiButton>
|
||||
</form>
|
||||
</UiCard>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h2>موجودی کالا</h2>
|
||||
<p v-if="!summary.products.length" class="muted">محصولی نیست.</p>
|
||||
<table v-else>
|
||||
<div class="inventory-grid">
|
||||
<UiCard>
|
||||
<template #header>
|
||||
<div class="section-title">
|
||||
<UiIcon name="box" :size="18" />
|
||||
<h3>موجودی کالا</h3>
|
||||
</div>
|
||||
</template>
|
||||
<p v-if="!summary.products.length" class="muted empty-note">محصولی در انبار تعریف نشده است.</p>
|
||||
<table v-else class="crm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>کالا</th>
|
||||
<th>نام کالا</th>
|
||||
<th>واحد</th>
|
||||
<th>موجودی</th>
|
||||
</tr>
|
||||
@@ -183,190 +195,185 @@ onMounted(load);
|
||||
<tr v-for="p in summary.products" :key="p.productId">
|
||||
<td>
|
||||
<strong>{{ p.name }}</strong>
|
||||
<small v-if="p.sku">{{ p.sku }}</small>
|
||||
<small v-if="p.sku" class="sku-hint">{{ p.sku }}</small>
|
||||
</td>
|
||||
<td>{{ p.unit || 'عدد' }}</td>
|
||||
<td class="num" :class="{ low: p.stockQty <= 0 }">
|
||||
{{ toFaDigits(String(p.stockQty)) }}
|
||||
<td>
|
||||
<UiBadge :variant="p.stockQty > 0 ? 'success' : 'danger'" size="sm">
|
||||
{{ toFaDigits(String(p.stockQty)) }}
|
||||
</UiBadge>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</UiCard>
|
||||
|
||||
<div class="card">
|
||||
<h2>گردش اخیر</h2>
|
||||
<p v-if="!movements.length" class="muted">گردشی ثبت نشده.</p>
|
||||
<ul v-else class="moves">
|
||||
<UiCard>
|
||||
<template #header>
|
||||
<div class="section-title">
|
||||
<UiIcon name="clock" :size="18" />
|
||||
<h3>گردش اخیر کالا</h3>
|
||||
</div>
|
||||
</template>
|
||||
<p v-if="!movements.length" class="muted empty-note">هیچ گردشی ثبت نشده است.</p>
|
||||
<ul v-else class="moves-list">
|
||||
<li v-for="m in movements" :key="m.id">
|
||||
<div>
|
||||
<div class="move-info">
|
||||
<strong>{{ m.productName }}</strong>
|
||||
<span>{{ typeLabel[m.type] }}</span>
|
||||
<small v-if="m.note">{{ m.note }}</small>
|
||||
<span class="move-type-tag">{{ typeLabel[m.type] }}</span>
|
||||
<small v-if="m.note" class="move-note">{{ m.note }}</small>
|
||||
</div>
|
||||
<div class="move-end">
|
||||
<UiBadge :variant="m.quantity > 0 ? 'success' : 'danger'" size="sm">
|
||||
{{ m.quantity > 0 ? '+' : '' }}{{ toFaDigits(String(m.quantity)) }}
|
||||
</UiBadge>
|
||||
<time class="move-time">{{ formatJalaliNumeric(new Date(m.occurredAt)) }}</time>
|
||||
</div>
|
||||
<em :class="{ in: m.quantity > 0, out: m.quantity < 0 }">
|
||||
{{ m.quantity > 0 ? '+' : '' }}{{ toFaDigits(String(m.quantity)) }}
|
||||
</em>
|
||||
<time>{{ formatJalaliNumeric(new Date(m.occurredAt)) }}</time>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</UiCard>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
.error-msg {
|
||||
color: var(--color-danger);
|
||||
font-weight: 600;
|
||||
font-size: var(--fs-sm);
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.sub,
|
||||
.muted {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.88rem;
|
||||
.notice-box {
|
||||
background: var(--color-info-surface);
|
||||
border: 1px solid var(--color-info);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--sp-3) var(--sp-4);
|
||||
color: var(--color-info);
|
||||
font-size: var(--fs-sm);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--danger, #dc2626);
|
||||
.kpis-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--sp-3);
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
display: grid;
|
||||
gap: var(--sp-1);
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
display: block;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--fs-xs);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.notice,
|
||||
.kpis article,
|
||||
.card,
|
||||
.move-form {
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem 1.1rem;
|
||||
.kpi-value {
|
||||
font-size: var(--fs-lg);
|
||||
color: var(--color-text-emphasis);
|
||||
}
|
||||
|
||||
.kpis {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.kpis span {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.kpis strong {
|
||||
font-size: 1.05rem;
|
||||
.move-form-card {
|
||||
margin: var(--sp-2) 0;
|
||||
}
|
||||
|
||||
.move-form {
|
||||
display: grid;
|
||||
grid-template-columns: 10rem 1fr 8rem 1fr auto;
|
||||
gap: 0.5rem;
|
||||
gap: var(--sp-3);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.move-form select {
|
||||
min-height: var(--control-h);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 0 0.6rem;
|
||||
font: inherit;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.grid {
|
||||
.inventory-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
gap: var(--sp-4);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: right;
|
||||
padding: 0.45rem 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
td small {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.num {
|
||||
.section-title h3 {
|
||||
margin: 0;
|
||||
font-size: var(--fs-base);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.low {
|
||||
color: var(--danger, #dc2626);
|
||||
.sku-hint {
|
||||
display: block;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--fs-xs);
|
||||
}
|
||||
|
||||
.moves {
|
||||
.empty-note {
|
||||
font-size: var(--fs-sm);
|
||||
padding: var(--sp-4) 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.moves-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
gap: var(--sp-3);
|
||||
}
|
||||
|
||||
.moves li {
|
||||
.moves-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-3);
|
||||
padding: var(--sp-2) 0;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.move-info {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 0.15rem 0.75rem;
|
||||
font-size: 0.86rem;
|
||||
gap: var(--sp-1);
|
||||
}
|
||||
|
||||
.moves span,
|
||||
.moves small,
|
||||
.moves time {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 0.78rem;
|
||||
.move-info strong {
|
||||
font-size: var(--fs-sm);
|
||||
color: var(--color-text-emphasis);
|
||||
}
|
||||
|
||||
.moves em {
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
grid-row: span 2;
|
||||
align-self: center;
|
||||
.move-type-tag {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--fs-xs);
|
||||
}
|
||||
|
||||
.moves .in {
|
||||
color: #16a34a;
|
||||
.move-note {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--fs-xs);
|
||||
}
|
||||
|
||||
.moves .out {
|
||||
color: #dc2626;
|
||||
.move-end {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: var(--sp-1);
|
||||
}
|
||||
|
||||
.move-time {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--fs-xs);
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.kpis,
|
||||
.grid,
|
||||
.kpis-grid,
|
||||
.inventory-grid,
|
||||
.move-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user