ADD: Standalone transaction creation page with sidebar menu entry
- Add CreateWarehouseTransaction.vue — full-page form to register IN/OUT transactions with item/warehouse dropdowns (searchable), quantity input, reason presets, date picker, live stock preview showing current balance for the selected item+warehouse combo, and input validation with inline error messages - Add /warehouse-items/create-transaction route - Add 'ثبت تراکنش' as first item in 'مدیریت انبار' sidebar group
This commit is contained in:
@@ -25,6 +25,7 @@ const model = ref([
|
|||||||
{
|
{
|
||||||
label: 'مدیریت انبار',
|
label: 'مدیریت انبار',
|
||||||
items: [
|
items: [
|
||||||
|
{ label: 'ثبت تراکنش', icon: 'pi pi-fw pi-plus-circle', to: '/warehouse-items/create-transaction' },
|
||||||
{ label: 'اقلام انبار', icon: 'pi pi-fw pi-box', to: '/warehouse-items/list' },
|
{ label: 'اقلام انبار', icon: 'pi pi-fw pi-box', to: '/warehouse-items/list' },
|
||||||
{ label: 'تراکنشهای انبار', icon: 'pi pi-fw pi-history', to: '/warehouse-items/transactions' },
|
{ label: 'تراکنشهای انبار', icon: 'pi pi-fw pi-history', to: '/warehouse-items/transactions' },
|
||||||
{ label: 'مکانهای انبار', icon: 'pi pi-fw pi-map', to: '/warehouse-items/warehouses' }
|
{ label: 'مکانهای انبار', icon: 'pi pi-fw pi-map', to: '/warehouse-items/warehouses' }
|
||||||
|
|||||||
@@ -100,6 +100,11 @@ const router = createRouter({
|
|||||||
name: 'warehouse-item-stock',
|
name: 'warehouse-item-stock',
|
||||||
component: () => import('@/views/pages/WarehouseItems/WarehouseItemStockView.vue')
|
component: () => import('@/views/pages/WarehouseItems/WarehouseItemStockView.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/warehouse-items/create-transaction',
|
||||||
|
name: 'create-warehouse-transaction',
|
||||||
|
component: () => import('@/views/pages/WarehouseItems/CreateWarehouseTransaction.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/warehouse-items/transactions',
|
path: '/warehouse-items/transactions',
|
||||||
name: 'warehouse-item-transactions',
|
name: 'warehouse-item-transactions',
|
||||||
|
|||||||
@@ -0,0 +1,572 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref, computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { WarehouseItemService } from '@/service/WarehouseItemService';
|
||||||
|
import { WarehouseService } from '@/service/WarehouseService';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const items = ref([]);
|
||||||
|
const warehouses = ref([]);
|
||||||
|
const submitting = ref(false);
|
||||||
|
const submitted = ref(false);
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
itemId: '',
|
||||||
|
warehouseId: '',
|
||||||
|
type: 'IN',
|
||||||
|
quantity: null,
|
||||||
|
reason: '',
|
||||||
|
note: '',
|
||||||
|
date: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stock preview for the selected item+warehouse combo
|
||||||
|
const stockPreview = ref(null);
|
||||||
|
const stockLoading = ref(false);
|
||||||
|
|
||||||
|
const IN_REASONS = [
|
||||||
|
'خرید از تامینکننده',
|
||||||
|
'برگشت از کارگاه',
|
||||||
|
'انتقال از انبار دیگر',
|
||||||
|
'اهدا / دریافت رایگان',
|
||||||
|
'موجودی اولیه',
|
||||||
|
'سایر',
|
||||||
|
];
|
||||||
|
|
||||||
|
const OUT_REASONS = [
|
||||||
|
'مصرف در پروژه',
|
||||||
|
'انتقال به انبار دیگر',
|
||||||
|
'فروش / واگذاری',
|
||||||
|
'ضایعات',
|
||||||
|
'مفقودی',
|
||||||
|
'سایر',
|
||||||
|
];
|
||||||
|
|
||||||
|
const reasons = computed(() => form.value.type === 'IN' ? IN_REASONS : OUT_REASONS);
|
||||||
|
|
||||||
|
const selectedItem = computed(() => items.value.find(i => i._id === form.value.itemId));
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([loadItems(), loadWarehouses()]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadItems = async () => {
|
||||||
|
try {
|
||||||
|
const res = await WarehouseItemService.listItems({ limit: 500, isActive: 'true' });
|
||||||
|
items.value = res.items || [];
|
||||||
|
} catch (e) {
|
||||||
|
toast.add({ severity: 'error', summary: 'خطا', detail: 'خطا در بارگذاری اقلام', life: 3000 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadWarehouses = async () => {
|
||||||
|
try {
|
||||||
|
warehouses.value = await WarehouseService.listWarehouses({ isActive: 'true' });
|
||||||
|
} catch (e) {
|
||||||
|
toast.add({ severity: 'error', summary: 'خطا', detail: 'خطا در بارگذاری انبارها', life: 3000 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load current stock when both item and warehouse are selected
|
||||||
|
const onItemOrWarehouseChange = async () => {
|
||||||
|
if (!form.value.itemId || !form.value.warehouseId) {
|
||||||
|
stockPreview.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stockLoading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await WarehouseItemService.getStockByItem(form.value.itemId);
|
||||||
|
const warehouseStock = data.stocks.find(s => s.warehouse?._id === form.value.warehouseId);
|
||||||
|
stockPreview.value = {
|
||||||
|
quantity: warehouseStock ? warehouseStock.quantity : 0,
|
||||||
|
unit: data.unit,
|
||||||
|
unitEn: data.unitEn,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
stockPreview.value = null;
|
||||||
|
} finally {
|
||||||
|
stockLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTypeChange = () => {
|
||||||
|
form.value.reason = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValid = computed(() => {
|
||||||
|
return form.value.itemId && form.value.warehouseId && form.value.quantity > 0 && form.value.reason;
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitTransaction = async () => {
|
||||||
|
submitted.value = true;
|
||||||
|
if (!isValid.value) {
|
||||||
|
toast.add({ severity: 'warn', summary: 'توجه', detail: 'لطفاً تمام فیلدهای ضروری را پر کنید', life: 3000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
await WarehouseItemService.createTransaction({
|
||||||
|
itemId: form.value.itemId,
|
||||||
|
warehouseId: form.value.warehouseId,
|
||||||
|
type: form.value.type,
|
||||||
|
quantity: form.value.quantity,
|
||||||
|
reason: form.value.reason,
|
||||||
|
note: form.value.note,
|
||||||
|
date: form.value.date || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'موفق',
|
||||||
|
detail: form.value.type === 'IN' ? 'موجودی با موفقیت به انبار افزوده شد' : 'موجودی با موفقیت از انبار کسر شد',
|
||||||
|
life: 4000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
form.value = { itemId: '', warehouseId: '', type: 'IN', quantity: null, reason: '', note: '', date: '' };
|
||||||
|
submitted.value = false;
|
||||||
|
stockPreview.value = null;
|
||||||
|
} catch (e) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'خطا',
|
||||||
|
detail: e.response?.data?.message || 'عملیات ناموفق بود',
|
||||||
|
life: 5000,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToStockView = () => {
|
||||||
|
if (form.value.itemId) {
|
||||||
|
router.push(`/warehouse-items/stock/${form.value.itemId}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="create-transaction-page">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<div class="header-icon" :class="form.type === 'IN' ? 'icon-in' : 'icon-out'">
|
||||||
|
<i :class="form.type === 'IN' ? 'pi pi-arrow-circle-down' : 'pi pi-arrow-circle-up'"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="page-title">ثبت تراکنش انبار</h1>
|
||||||
|
<p class="page-subtitle">ورود یا خروج اقلام از انبار</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<Button
|
||||||
|
icon="pi pi-history"
|
||||||
|
label="تاریخچه تراکنشها"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
@click="router.push('/warehouse-items/transactions')"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="form.itemId"
|
||||||
|
icon="pi pi-chart-bar"
|
||||||
|
label="مشاهده موجودی قلم"
|
||||||
|
severity="info"
|
||||||
|
outlined
|
||||||
|
@click="goToStockView"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-layout">
|
||||||
|
|
||||||
|
<!-- Main form card -->
|
||||||
|
<div class="card form-card">
|
||||||
|
|
||||||
|
<!-- Type selector -->
|
||||||
|
<div class="type-selector">
|
||||||
|
<button
|
||||||
|
id="type-in"
|
||||||
|
class="type-card"
|
||||||
|
:class="{ active: form.type === 'IN' }"
|
||||||
|
type="button"
|
||||||
|
@click="form.type = 'IN'; onTypeChange()"
|
||||||
|
>
|
||||||
|
<div class="type-icon in-icon"><i class="pi pi-arrow-down"></i></div>
|
||||||
|
<div class="type-text">
|
||||||
|
<span class="type-label">ورود به انبار</span>
|
||||||
|
<span class="type-desc">افزودن موجودی</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="type-out"
|
||||||
|
class="type-card"
|
||||||
|
:class="{ active: form.type === 'OUT' }"
|
||||||
|
type="button"
|
||||||
|
@click="form.type = 'OUT'; onTypeChange()"
|
||||||
|
>
|
||||||
|
<div class="type-icon out-icon"><i class="pi pi-arrow-up"></i></div>
|
||||||
|
<div class="type-text">
|
||||||
|
<span class="type-label">خروج از انبار</span>
|
||||||
|
<span class="type-desc">کسر موجودی</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<!-- Item & Warehouse row -->
|
||||||
|
<div class="fields-grid">
|
||||||
|
<div class="form-field" :class="{ 'field-error': submitted && !form.itemId }">
|
||||||
|
<label for="item-select">قلم انبار <span class="required">*</span></label>
|
||||||
|
<Select
|
||||||
|
id="item-select"
|
||||||
|
v-model="form.itemId"
|
||||||
|
:options="items"
|
||||||
|
optionLabel="nameFa"
|
||||||
|
optionValue="_id"
|
||||||
|
placeholder="انتخاب قلم..."
|
||||||
|
class="w-full"
|
||||||
|
filter
|
||||||
|
filterPlaceholder="جستجو..."
|
||||||
|
@change="onItemOrWarehouseChange"
|
||||||
|
>
|
||||||
|
<template #option="{ option }">
|
||||||
|
<div class="item-option">
|
||||||
|
<span class="option-fa">{{ option.nameFa }}</span>
|
||||||
|
<span class="option-en">{{ option.nameEn }}</span>
|
||||||
|
<span class="option-unit">{{ option.unitFa }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Select>
|
||||||
|
<small class="error-msg" v-if="submitted && !form.itemId">قلم انبار ضروری است</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field" :class="{ 'field-error': submitted && !form.warehouseId }">
|
||||||
|
<label for="warehouse-select">انبار <span class="required">*</span></label>
|
||||||
|
<Select
|
||||||
|
id="warehouse-select"
|
||||||
|
v-model="form.warehouseId"
|
||||||
|
:options="warehouses"
|
||||||
|
optionLabel="name"
|
||||||
|
optionValue="_id"
|
||||||
|
placeholder="انتخاب انبار..."
|
||||||
|
class="w-full"
|
||||||
|
@change="onItemOrWarehouseChange"
|
||||||
|
>
|
||||||
|
<template #option="{ option }">
|
||||||
|
<div class="warehouse-option">
|
||||||
|
<span>{{ option.name }}</span>
|
||||||
|
<span v-if="option.location?.address" class="option-addr">{{ option.location.address }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Select>
|
||||||
|
<small class="error-msg" v-if="submitted && !form.warehouseId">انبار ضروری است</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stock preview banner -->
|
||||||
|
<Transition name="fade">
|
||||||
|
<div v-if="stockPreview !== null" class="stock-preview" :class="{ 'stock-zero': stockPreview.quantity <= 0 }">
|
||||||
|
<i class="pi pi-database"></i>
|
||||||
|
<span>
|
||||||
|
موجودی فعلی در این انبار:
|
||||||
|
<strong>{{ stockPreview.quantity.toLocaleString('fa-IR') }} {{ stockPreview.unit }}</strong>
|
||||||
|
<span class="unit-en-small">({{ stockPreview.unitEn }})</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="stockPreview.quantity <= 0 && form.type === 'OUT'" class="stock-alert">
|
||||||
|
<i class="pi pi-exclamation-triangle"></i> موجودی صفر است
|
||||||
|
</span>
|
||||||
|
<ProgressSpinner v-if="stockLoading" style="width:20px;height:20px" strokeWidth="5" />
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<!-- Quantity & Date row -->
|
||||||
|
<div class="fields-grid">
|
||||||
|
<div class="form-field" :class="{ 'field-error': submitted && !form.quantity }">
|
||||||
|
<label for="quantity-input">
|
||||||
|
مقدار
|
||||||
|
<span v-if="selectedItem" class="unit-hint">({{ selectedItem.unitFa }} / {{ selectedItem.unitEn }})</span>
|
||||||
|
<span class="required">*</span>
|
||||||
|
</label>
|
||||||
|
<InputNumber
|
||||||
|
id="quantity-input"
|
||||||
|
v-model="form.quantity"
|
||||||
|
:min="0.001"
|
||||||
|
:minFractionDigits="0"
|
||||||
|
:maxFractionDigits="3"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="مقدار را وارد کنید"
|
||||||
|
:pt="{ input: { dir: 'ltr', style: 'font-size:1.15rem; font-weight:600;' } }"
|
||||||
|
/>
|
||||||
|
<small class="error-msg" v-if="submitted && !form.quantity">مقدار ضروری است</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="date-input">تاریخ تراکنش <span class="optional">(اختیاری — پیشفرض: اکنون)</span></label>
|
||||||
|
<InputText id="date-input" v-model="form.date" type="datetime-local" class="w-full" dir="ltr" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reason -->
|
||||||
|
<div class="form-field" :class="{ 'field-error': submitted && !form.reason }">
|
||||||
|
<label>دلیل تراکنش <span class="required">*</span></label>
|
||||||
|
<div class="reason-chips">
|
||||||
|
<button
|
||||||
|
v-for="r in reasons"
|
||||||
|
:key="r"
|
||||||
|
class="reason-chip"
|
||||||
|
:class="{ active: form.reason === r }"
|
||||||
|
type="button"
|
||||||
|
@click="form.reason = r"
|
||||||
|
>{{ r }}</button>
|
||||||
|
</div>
|
||||||
|
<InputText
|
||||||
|
v-model="form.reason"
|
||||||
|
placeholder="یا دلیل سفارشی وارد کنید..."
|
||||||
|
class="w-full mt-2"
|
||||||
|
:class="{ 'p-invalid': submitted && !form.reason }"
|
||||||
|
/>
|
||||||
|
<small class="error-msg" v-if="submitted && !form.reason">دلیل تراکنش ضروری است</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Note -->
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="note-input">یادداشت <span class="optional">(اختیاری)</span></label>
|
||||||
|
<Textarea id="note-input" v-model="form.note" rows="2" class="w-full" autoResize placeholder="توضیحات تکمیلی..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit -->
|
||||||
|
<div class="submit-row">
|
||||||
|
<Button
|
||||||
|
label="انصراف و پاککردن فرم"
|
||||||
|
icon="pi pi-times"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
@click="() => { form = { itemId: '', warehouseId: '', type: 'IN', quantity: null, reason: '', note: '', date: '' }; submitted = false; stockPreview = null; }"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
:label="form.type === 'IN' ? 'ثبت ورود به انبار' : 'ثبت خروج از انبار'"
|
||||||
|
:icon="form.type === 'IN' ? 'pi pi-plus-circle' : 'pi pi-minus-circle'"
|
||||||
|
:severity="form.type === 'IN' ? 'success' : 'danger'"
|
||||||
|
:loading="submitting"
|
||||||
|
size="large"
|
||||||
|
class="submit-btn"
|
||||||
|
@click="submitTransaction"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Side info card -->
|
||||||
|
<div class="side-panel">
|
||||||
|
<div class="card info-card">
|
||||||
|
<h4 class="info-title"><i class="pi pi-info-circle"></i> راهنما</h4>
|
||||||
|
<ul class="info-list">
|
||||||
|
<li>
|
||||||
|
<i class="pi pi-arrow-down in-dot"></i>
|
||||||
|
<span><strong>ورود:</strong> کالایی وارد انبار میشود (خرید، برگشت کالا، انتقال از جای دیگر)</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="pi pi-arrow-up out-dot"></i>
|
||||||
|
<span><strong>خروج:</strong> کالایی از انبار برداشت میشود (مصرف، فروش، ضایعات)</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="pi pi-lock"></i>
|
||||||
|
<span>اگر موجودی کافی نباشد، تراکنش خروج ثبت نمیشود</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="pi pi-user"></i>
|
||||||
|
<span>کاربر انجامدهنده به صورت خودکار ثبت میشود</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card quick-links-card" v-if="form.itemId">
|
||||||
|
<h4 class="info-title"><i class="pi pi-link"></i> دسترسی سریع</h4>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-chart-bar"
|
||||||
|
label="موجودی این قلم در همه انبارها"
|
||||||
|
class="w-full mb-2"
|
||||||
|
severity="info"
|
||||||
|
outlined
|
||||||
|
@click="goToStockView"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-box"
|
||||||
|
label="لیست همه اقلام"
|
||||||
|
class="w-full"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
@click="router.push('/warehouse-items/list')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.create-transaction-page { padding: 1.5rem; direction: rtl; }
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.page-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem;
|
||||||
|
}
|
||||||
|
.header-left { display: flex; align-items: center; gap: 1rem; }
|
||||||
|
.header-icon {
|
||||||
|
width: 56px; height: 56px; border-radius: 16px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 1.6rem; color: #fff;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.icon-in {
|
||||||
|
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||||||
|
box-shadow: 0 4px 15px rgba(34,197,94,0.35);
|
||||||
|
}
|
||||||
|
.icon-out {
|
||||||
|
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||||
|
box-shadow: 0 4px 15px rgba(239,68,68,0.35);
|
||||||
|
}
|
||||||
|
.page-title { margin: 0; font-size: 1.5rem; font-weight: 700; }
|
||||||
|
.page-subtitle { margin: 0; font-size: 0.875rem; color: var(--text-color-secondary); }
|
||||||
|
.header-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
.form-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 300px;
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.form-layout { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form card */
|
||||||
|
.form-card { border-radius: 16px; display: flex; flex-direction: column; gap: 1.25rem; }
|
||||||
|
|
||||||
|
/* Type selector */
|
||||||
|
.type-selector { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||||
|
.type-card {
|
||||||
|
border: 2px solid var(--surface-border);
|
||||||
|
background: var(--surface-50);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
transition: all 0.25s;
|
||||||
|
text-align: right;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
.type-card:hover { border-color: var(--primary-color); transform: translateY(-1px); }
|
||||||
|
.type-card.active { transform: translateY(-2px); box-shadow: 0 6px 20px -5px currentColor; }
|
||||||
|
.type-card:nth-child(1).active { border-color: #22c55e; background: rgba(34,197,94,0.07); box-shadow: 0 6px 20px -5px rgba(34,197,94,0.4); }
|
||||||
|
.type-card:nth-child(2).active { border-color: #ef4444; background: rgba(239,68,68,0.07); box-shadow: 0 6px 20px -5px rgba(239,68,68,0.4); }
|
||||||
|
|
||||||
|
.type-icon {
|
||||||
|
width: 44px; height: 44px; border-radius: 12px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 1.3rem; color: #fff; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.in-icon { background: linear-gradient(135deg, #22c55e, #16a34a); }
|
||||||
|
.out-icon { background: linear-gradient(135deg, #ef4444, #dc2626); }
|
||||||
|
.type-text { display: flex; flex-direction: column; gap: 2px; }
|
||||||
|
.type-label { font-weight: 700; font-size: 1rem; }
|
||||||
|
.type-desc { font-size: 0.8rem; color: var(--text-color-secondary); }
|
||||||
|
|
||||||
|
/* Fields */
|
||||||
|
.fields-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||||
|
@media (max-width: 600px) { .fields-grid { grid-template-columns: 1fr; } }
|
||||||
|
.form-field { display: flex; flex-direction: column; gap: 0.4rem; }
|
||||||
|
.form-field label { font-weight: 600; font-size: 0.9rem; }
|
||||||
|
.required { color: #ef4444; }
|
||||||
|
.optional { font-size: 0.75rem; color: var(--text-color-secondary); font-weight: 400; }
|
||||||
|
.unit-hint { font-size: 0.8rem; color: #6366f1; font-weight: 600; margin-right: 0.25rem; }
|
||||||
|
.error-msg { color: #ef4444; font-size: 0.8rem; }
|
||||||
|
.field-error label { color: #ef4444; }
|
||||||
|
|
||||||
|
/* Stock preview */
|
||||||
|
.stock-preview {
|
||||||
|
display: flex; align-items: center; gap: 0.75rem;
|
||||||
|
background: rgba(99,102,241,0.08);
|
||||||
|
border: 1px solid rgba(99,102,241,0.25);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.stock-preview.stock-zero {
|
||||||
|
background: rgba(239,68,68,0.08);
|
||||||
|
border-color: rgba(239,68,68,0.3);
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
.stock-alert { margin-right: auto; font-size: 0.8rem; font-weight: 600; color: #ef4444; }
|
||||||
|
.unit-en-small { font-size: 0.8rem; color: var(--text-color-secondary); margin-right: 0.25rem; }
|
||||||
|
|
||||||
|
/* Reason chips */
|
||||||
|
.reason-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }
|
||||||
|
.reason-chip {
|
||||||
|
border: 1.5px solid var(--surface-border);
|
||||||
|
background: var(--surface-50);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 5px 14px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
.reason-chip:hover { border-color: var(--primary-color); color: var(--primary-color); }
|
||||||
|
.reason-chip.active {
|
||||||
|
background: rgba(99,102,241,0.1);
|
||||||
|
border-color: #6366f1;
|
||||||
|
color: #6366f1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Submit row */
|
||||||
|
.submit-row {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
padding-top: 0.5rem; gap: 1rem;
|
||||||
|
}
|
||||||
|
.submit-btn { flex: 0 0 auto; }
|
||||||
|
|
||||||
|
/* Dropdown option templates */
|
||||||
|
.item-option { display: flex; align-items: center; gap: 0.5rem; }
|
||||||
|
.option-fa { font-weight: 600; }
|
||||||
|
.option-en { font-size: 0.8rem; color: var(--text-color-secondary); direction: ltr; }
|
||||||
|
.option-unit {
|
||||||
|
margin-right: auto;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
background: rgba(99,102,241,0.1);
|
||||||
|
color: #6366f1;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.warehouse-option { display: flex; flex-direction: column; }
|
||||||
|
.option-addr { font-size: 0.75rem; color: var(--text-color-secondary); }
|
||||||
|
|
||||||
|
/* Side panel */
|
||||||
|
.side-panel { display: flex; flex-direction: column; gap: 1rem; }
|
||||||
|
.info-card, .quick-links-card { border-radius: 14px; }
|
||||||
|
.info-title {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex; align-items: center; gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.info-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.75rem; }
|
||||||
|
.info-list li { display: flex; align-items: flex-start; gap: 0.6rem; font-size: 0.85rem; line-height: 1.5; }
|
||||||
|
.in-dot { color: #22c55e; margin-top: 2px; }
|
||||||
|
.out-dot { color: #ef4444; margin-top: 2px; }
|
||||||
|
|
||||||
|
/* Transition */
|
||||||
|
.fade-enter-active, .fade-leave-active { transition: all 0.25s ease; }
|
||||||
|
.fade-enter-from { opacity: 0; transform: translateY(-6px); }
|
||||||
|
.fade-leave-to { opacity: 0; transform: translateY(-6px); }
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user