feat: complete admin url query sync, full sorting/filters, latin search, product media players, personalized pet name and stage noindex
All checks were successful
Deploy Canina / deploy (push) Successful in 1m57s
All checks were successful
Deploy Canina / deploy (push) Successful in 1m57s
This commit is contained in:
parent
d5c05b7577
commit
27a1517464
@ -20,6 +20,11 @@ export class PaginationQuery {
|
|||||||
role?: string;
|
role?: string;
|
||||||
categoryId?: string;
|
categoryId?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
sortBy?: string;
|
||||||
|
sortOrder?: 'asc' | 'desc';
|
||||||
|
suitableFor?: string;
|
||||||
|
minPrice?: number | string;
|
||||||
|
maxPrice?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CouponTargetInput {
|
export class CouponTargetInput {
|
||||||
@ -114,12 +119,17 @@ export class AdminService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortField = (query.sortBy && typeof query.sortBy === 'string' && ['createdAt', 'firstName', 'lastName', 'mobile', 'email', 'role', 'walletBalance'].includes(query.sortBy))
|
||||||
|
? query.sortBy
|
||||||
|
: 'createdAt';
|
||||||
|
const sortDirection = query.sortOrder === 'asc' ? 'asc' : 'desc';
|
||||||
|
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
this.prisma.user.findMany({
|
this.prisma.user.findMany({
|
||||||
where,
|
where,
|
||||||
skip,
|
skip,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { [sortField]: sortDirection },
|
||||||
include: {
|
include: {
|
||||||
addresses: true,
|
addresses: true,
|
||||||
walletTransactions: {
|
walletTransactions: {
|
||||||
@ -274,22 +284,59 @@ export class AdminService {
|
|||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
const where: Prisma.ProductWhereInput = {};
|
const where: Prisma.ProductWhereInput = {};
|
||||||
|
const andConditions: Prisma.ProductWhereInput[] = [];
|
||||||
|
|
||||||
if (query.search) {
|
if (query.search) {
|
||||||
where.OR = [
|
const trimmed = query.search.trim();
|
||||||
{ nameFa: { contains: query.search, mode: 'insensitive' } },
|
andConditions.push({
|
||||||
{ artNo: { contains: query.search } },
|
OR: [
|
||||||
];
|
{ nameFa: { contains: trimmed, mode: 'insensitive' } },
|
||||||
|
{ nameEn: { contains: trimmed, mode: 'insensitive' } },
|
||||||
|
{ artNo: { contains: trimmed, mode: 'insensitive' } },
|
||||||
|
{ barcode: { contains: trimmed, mode: 'insensitive' } },
|
||||||
|
{ slug: { contains: trimmed, mode: 'insensitive' } },
|
||||||
|
{ description: { contains: trimmed, mode: 'insensitive' } },
|
||||||
|
{ shortDescription: { contains: trimmed, mode: 'insensitive' } },
|
||||||
|
{ scientificTagline: { contains: trimmed, mode: 'insensitive' } },
|
||||||
|
{
|
||||||
|
symptoms: {
|
||||||
|
some: {
|
||||||
|
symptom: { contains: trimmed, mode: 'insensitive' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: {
|
||||||
|
name: { contains: trimmed, mode: 'insensitive' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.categoryId) {
|
if (query.categoryId) {
|
||||||
where.categoryId = query.categoryId;
|
andConditions.push({ categoryId: query.categoryId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.suitableFor && query.suitableFor !== 'all') {
|
||||||
|
andConditions.push({ suitableFor: { in: [query.suitableFor, 'سگ و گربه', 'هر دو'] } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (andConditions.length > 0) {
|
||||||
|
where.AND = andConditions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortField = (query.sortBy && typeof query.sortBy === 'string' && ['nameFa', 'artNo', 'priceValue', 'packageSize', 'createdAt', 'updatedAt'].includes(query.sortBy))
|
||||||
|
? query.sortBy
|
||||||
|
: 'createdAt';
|
||||||
|
const sortDirection = query.sortOrder === 'asc' ? 'asc' : 'desc';
|
||||||
|
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
this.prisma.product.findMany({
|
this.prisma.product.findMany({
|
||||||
where,
|
where,
|
||||||
skip,
|
skip,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { [sortField]: sortDirection },
|
||||||
include: { category: true, symptoms: true },
|
include: { category: true, symptoms: true },
|
||||||
}),
|
}),
|
||||||
this.prisma.product.count({ where }),
|
this.prisma.product.count({ where }),
|
||||||
@ -441,12 +488,17 @@ export class AdminService {
|
|||||||
where.status = query.status;
|
where.status = query.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortField = (query.sortBy && typeof query.sortBy === 'string' && ['createdAt', 'totalAmount', 'status', 'finalAmount'].includes(query.sortBy))
|
||||||
|
? query.sortBy
|
||||||
|
: 'createdAt';
|
||||||
|
const sortDirection = query.sortOrder === 'asc' ? 'asc' : 'desc';
|
||||||
|
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
this.prisma.order.findMany({
|
this.prisma.order.findMany({
|
||||||
where,
|
where,
|
||||||
skip,
|
skip,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { [sortField]: sortDirection },
|
||||||
include: {
|
include: {
|
||||||
user: true,
|
user: true,
|
||||||
orderItems: {
|
orderItems: {
|
||||||
|
|||||||
@ -326,7 +326,7 @@ export default function MediaSelector({ isOpen, onClose, onSelect, multiple = fa
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Upload className="w-4 h-4" />
|
<Upload className="w-4 h-4" />
|
||||||
<span>آپلود فایل جدید (همگانی)</span>
|
<span>آپلود فایل جدید</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
Eye,
|
Eye,
|
||||||
@ -18,7 +19,10 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
PackageCheck,
|
PackageCheck,
|
||||||
Heart,
|
Heart,
|
||||||
CreditCard
|
CreditCard,
|
||||||
|
ArrowUpDown,
|
||||||
|
ArrowUp,
|
||||||
|
ArrowDown
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Skeleton from '../components/ui/Skeleton';
|
import Skeleton from '../components/ui/Skeleton';
|
||||||
@ -58,44 +62,90 @@ export interface Order {
|
|||||||
id: string;
|
id: string;
|
||||||
trackingNumber?: string;
|
trackingNumber?: string;
|
||||||
status: string;
|
status: string;
|
||||||
createdAt?: string;
|
totalAmount: number;
|
||||||
date?: string;
|
finalAmount: number;
|
||||||
charityDonation?: number;
|
discountAmount: number;
|
||||||
totalAmount?: number;
|
shippingCost: number;
|
||||||
total?: number;
|
createdAt: string;
|
||||||
isRefill?: boolean;
|
updatedAt: string;
|
||||||
paymentMethod?: string;
|
itemsCount?: number;
|
||||||
address?: string;
|
|
||||||
shippingAddress?: string;
|
|
||||||
orderItems?: OrderItem[];
|
|
||||||
items?: OrderItem[];
|
|
||||||
paymentTransactions?: PaymentTx[];
|
|
||||||
user?: {
|
user?: {
|
||||||
|
id?: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
mobile?: string;
|
mobile?: string;
|
||||||
phone?: string;
|
|
||||||
email?: string;
|
email?: string;
|
||||||
};
|
};
|
||||||
|
address?: {
|
||||||
|
fullAddress?: string;
|
||||||
|
city?: string;
|
||||||
|
province?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
recipientName?: string;
|
||||||
|
recipientMobile?: string;
|
||||||
|
};
|
||||||
|
orderItems: OrderItem[];
|
||||||
|
items?: OrderItem[];
|
||||||
|
paymentTransactions?: PaymentTx[];
|
||||||
|
notes?: string;
|
||||||
|
shippingMethod?: string;
|
||||||
|
paymentMethod?: string;
|
||||||
|
date?: string;
|
||||||
|
charityDonation?: number;
|
||||||
|
total?: number;
|
||||||
|
isRefill?: boolean;
|
||||||
|
shippingAddress?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusStyles: Record<string, { label: string; color: string; icon: React.ElementType }> = {
|
export const ORDER_STATUS_MAP: Record<string, { label: string; color: string; icon: React.ElementType }> = {
|
||||||
pending_payment: { label: 'در انتظار پرداخت / ناموفق', color: 'bg-rose-100 text-rose-800 border-rose-200', icon: Clock },
|
pending_payment: { label: 'در انتظار پرداخت', color: 'bg-amber-50 text-amber-700 border-amber-200', icon: Clock },
|
||||||
pending: { label: 'در حال بررسی', color: 'bg-orange-100 text-orange-700 border-orange-200', icon: Clock },
|
pending: { label: 'در حال بررسی', color: 'bg-orange-100 text-orange-700 border-orange-200', icon: Clock },
|
||||||
processing: { label: 'در حال پردازش', color: 'bg-amber-100 text-amber-800 border-amber-200', icon: Clock },
|
processing: { label: 'در حال پردازش (جدید)', color: 'bg-blue-50 text-blue-700 border-blue-200', icon: Clock },
|
||||||
packaged: { label: 'بستهبندی شده (آماده ارسال)', color: 'bg-purple-100 text-purple-800 border-purple-200', icon: PackageCheck },
|
packaged: { label: 'بستهبندی شده (آماده ارسال)', color: 'bg-purple-100 text-purple-800 border-purple-200', icon: PackageCheck },
|
||||||
ready_to_ship: { label: 'آماده ارسال', color: 'bg-purple-100 text-purple-800 border-purple-200', icon: PackageCheck },
|
ready_to_ship: { label: 'آماده ارسال', color: 'bg-purple-100 text-purple-800 border-purple-200', icon: PackageCheck },
|
||||||
shipped: { label: 'ارسال شده', color: 'bg-blue-100 text-blue-700 border-blue-200', icon: Truck },
|
shipped: { label: 'ارسال شده', color: 'bg-indigo-50 text-indigo-700 border-indigo-200', icon: Truck },
|
||||||
delivered: { label: 'تحویل داده شده', color: 'bg-green-100 text-green-700 border-green-200', icon: CheckCircle2 },
|
delivered: { label: 'تحویل داده شده', color: 'bg-emerald-50 text-emerald-700 border-emerald-200', icon: CheckCircle2 },
|
||||||
cancelled: { label: 'لغو شده', color: 'bg-red-100 text-red-700 border-red-200', icon: XCircle },
|
cancelled: { label: 'لغو شده', color: 'bg-red-50 text-red-700 border-red-200', icon: XCircle },
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPaymentMethodLabel = (method?: string): { label: string; color: string } => {
|
export const getPaymentStatusBadge = (status: string) => {
|
||||||
switch (method) {
|
switch (status?.toLowerCase()) {
|
||||||
case 'wallet': return { label: 'کیف پول', color: 'bg-purple-50 text-purple-700 border-purple-200' };
|
case 'success':
|
||||||
|
case 'paid':
|
||||||
|
case 'completed':
|
||||||
|
return { label: 'پرداخت موفق', color: 'bg-emerald-50 text-emerald-700 border-emerald-200' };
|
||||||
|
case 'pending':
|
||||||
|
case 'in_progress':
|
||||||
|
return { label: 'در انتظار پرداخت', color: 'bg-amber-50 text-amber-700 border-amber-200' };
|
||||||
|
case 'failed':
|
||||||
|
case 'error':
|
||||||
|
return { label: 'ناموفق', color: 'bg-red-50 text-red-700 border-red-200' };
|
||||||
|
default:
|
||||||
|
return { label: 'نامشخص', color: 'bg-gray-50 text-gray-700 border-gray-200' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getShippingMethodLabel = (method?: string) => {
|
||||||
|
switch (method?.toLowerCase()) {
|
||||||
|
case 'express':
|
||||||
|
case 'tipax': return { label: 'تیپاکس (اکسپرس)', color: 'bg-purple-50 text-purple-700 border-purple-200' };
|
||||||
|
case 'post':
|
||||||
|
case 'pishtaz': return { label: 'پست پیشتاز', color: 'bg-blue-50 text-blue-700 border-blue-200' };
|
||||||
|
case 'courier':
|
||||||
|
case 'peyk': return { label: 'پیک اختصاصی فوری', color: 'bg-amber-50 text-amber-700 border-amber-200' };
|
||||||
|
default: return { label: method || 'پست پیشتاز', color: 'bg-gray-50 text-gray-700 border-gray-200' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPaymentMethodLabel = (method?: string) => {
|
||||||
|
switch (method?.toLowerCase()) {
|
||||||
|
case 'online':
|
||||||
case 'zibal':
|
case 'zibal':
|
||||||
case 'online': return { label: 'درگاه زیبال (آنلاین)', color: 'bg-emerald-50 text-emerald-700 border-emerald-200' };
|
case 'gateway': return { label: 'درگاه آنلاین (زیبال)', color: 'bg-emerald-50 text-emerald-700 border-emerald-200' };
|
||||||
case 'card_to_card': return { label: 'کارت به کارت', color: 'bg-blue-50 text-blue-700 border-blue-200' };
|
case 'wallet': return { label: 'کیف پول کنینا', color: 'bg-blue-50 text-blue-700 border-blue-200' };
|
||||||
|
case 'card':
|
||||||
|
case 'cart':
|
||||||
|
case 'card_to_card': return { label: 'کارت به کارت', color: 'bg-indigo-50 text-indigo-700 border-indigo-200' };
|
||||||
case 'cod': return { label: 'پرداخت در محل', color: 'bg-amber-50 text-amber-700 border-amber-200' };
|
case 'cod': return { label: 'پرداخت در محل', color: 'bg-amber-50 text-amber-700 border-amber-200' };
|
||||||
default: return { label: method || 'آنلاین', color: 'bg-gray-50 text-gray-700 border-gray-200' };
|
default: return { label: method || 'آنلاین', color: 'bg-gray-50 text-gray-700 border-gray-200' };
|
||||||
}
|
}
|
||||||
@ -111,13 +161,44 @@ export default function Orders() {
|
|||||||
const [orders, setOrders] = useState<Order[]>([]);
|
const [orders, setOrders] = useState<Order[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
// Queries
|
// URL Query Params Management
|
||||||
const [page, setPage] = useState(1);
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
const [status, setStatus] = useState('');
|
// Queries initialized from URL
|
||||||
|
const [page, setPage] = useState(() => Number(searchParams.get('page')) || 1);
|
||||||
|
const [search, setSearch] = useState(() => searchParams.get('search') || '');
|
||||||
|
const [status, setStatus] = useState(() => searchParams.get('status') || '');
|
||||||
|
const [sortBy, setSortBy] = useState(() => searchParams.get('sortBy') || 'createdAt');
|
||||||
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(() => (searchParams.get('sortOrder') as 'asc' | 'desc') || 'desc');
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const [processingId, setProcessingId] = useState<string | null>(null);
|
const [processingId, setProcessingId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Sync state changes to URL search params
|
||||||
|
const updateUrlParams = useCallback((paramsObj: Record<string, string | number | undefined | null>) => {
|
||||||
|
const current = Object.fromEntries(searchParams.entries());
|
||||||
|
const merged = { ...current, ...paramsObj };
|
||||||
|
|
||||||
|
const cleaned: Record<string, string> = {};
|
||||||
|
Object.entries(merged).forEach(([key, val]) => {
|
||||||
|
if (val !== undefined && val !== null && String(val).trim() !== '' && !(key === 'page' && String(val) === '1') && !(key === 'sortBy' && String(val) === 'createdAt') && !(key === 'sortOrder' && String(val) === 'desc')) {
|
||||||
|
cleaned[key] = String(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setSearchParams(cleaned, { replace: true });
|
||||||
|
}, [searchParams, setSearchParams]);
|
||||||
|
|
||||||
|
const handleSort = (field: string) => {
|
||||||
|
let newOrder: 'asc' | 'desc' = 'asc';
|
||||||
|
if (sortBy === field) {
|
||||||
|
newOrder = sortOrder === 'asc' ? 'desc' : 'asc';
|
||||||
|
}
|
||||||
|
setSortBy(field);
|
||||||
|
setSortOrder(newOrder);
|
||||||
|
setPage(1);
|
||||||
|
updateUrlParams({ sortBy: field, sortOrder: newOrder, page: 1 });
|
||||||
|
};
|
||||||
|
|
||||||
// Modal State
|
// Modal State
|
||||||
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);
|
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);
|
||||||
const [modalTrackingCode, setModalTrackingCode] = useState('');
|
const [modalTrackingCode, setModalTrackingCode] = useState('');
|
||||||
@ -131,13 +212,14 @@ export default function Orders() {
|
|||||||
params.append('page', page.toString());
|
params.append('page', page.toString());
|
||||||
if (search) params.append('search', search);
|
if (search) params.append('search', search);
|
||||||
if (status) params.append('status', status);
|
if (status) params.append('status', status);
|
||||||
|
if (sortBy) params.append('sortBy', sortBy);
|
||||||
|
if (sortOrder) params.append('sortOrder', sortOrder);
|
||||||
|
|
||||||
const response = await api.get(`/admin/orders?${params.toString()}`);
|
const response = await api.get(`/admin/orders?${params.toString()}`);
|
||||||
if (response.data?.success) {
|
if (response.data?.success) {
|
||||||
setOrders(response.data.data);
|
setOrders(response.data.data);
|
||||||
setTotalPages(response.data.meta?.lastPage || 1);
|
setTotalPages(response.data.meta?.lastPage || 1);
|
||||||
|
|
||||||
// Update selectedOrder if open without causing re-fetch loop
|
|
||||||
setSelectedOrder((currentSelected) => {
|
setSelectedOrder((currentSelected) => {
|
||||||
if (!currentSelected) return null;
|
if (!currentSelected) return null;
|
||||||
const updated = response.data.data.find((o: Order) => o.id === currentSelected.id);
|
const updated = response.data.data.find((o: Order) => o.id === currentSelected.id);
|
||||||
@ -149,12 +231,12 @@ export default function Orders() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [page, search, status]);
|
}, [page, search, status, sortBy, sortOrder]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const delayDebounceFn = setTimeout(() => {
|
const delayDebounceFn = setTimeout(() => {
|
||||||
fetchOrders();
|
fetchOrders();
|
||||||
}, 500);
|
}, 400);
|
||||||
|
|
||||||
return () => clearTimeout(delayDebounceFn);
|
return () => clearTimeout(delayDebounceFn);
|
||||||
}, [fetchOrders]);
|
}, [fetchOrders]);
|
||||||
@ -370,7 +452,12 @@ export default function Orders() {
|
|||||||
<select
|
<select
|
||||||
className="bg-white border border-gray-200 text-gray-700 px-4 py-2.5 rounded-xl flex items-center gap-2 font-bold hover:bg-gray-50 transition-all outline-none w-full sm:w-auto text-sm"
|
className="bg-white border border-gray-200 text-gray-700 px-4 py-2.5 rounded-xl flex items-center gap-2 font-bold hover:bg-gray-50 transition-all outline-none w-full sm:w-auto text-sm"
|
||||||
value={status}
|
value={status}
|
||||||
onChange={(e) => { setStatus(e.target.value); setPage(1); }}
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setStatus(val);
|
||||||
|
setPage(1);
|
||||||
|
updateUrlParams({ status: val, page: 1 });
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<option value="">همه وضعیتها</option>
|
<option value="">همه وضعیتها</option>
|
||||||
<option value="pending_payment">در انتظار پرداخت / ناموفق</option>
|
<option value="pending_payment">در انتظار پرداخت / ناموفق</option>
|
||||||
@ -388,7 +475,12 @@ export default function Orders() {
|
|||||||
className="pl-4 pr-10 py-2.5 border border-gray-200 rounded-xl outline-none focus:ring-2 focus:ring-purple-500 w-full font-medium text-sm"
|
className="pl-4 pr-10 py-2.5 border border-gray-200 rounded-xl outline-none focus:ring-2 focus:ring-purple-500 w-full font-medium text-sm"
|
||||||
dir="rtl"
|
dir="rtl"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setSearch(val);
|
||||||
|
setPage(1);
|
||||||
|
updateUrlParams({ search: val, page: 1 });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -409,7 +501,11 @@ export default function Orders() {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={tab.value}
|
key={tab.value}
|
||||||
onClick={() => { setStatus(tab.value); setPage(1); }}
|
onClick={() => {
|
||||||
|
setStatus(tab.value);
|
||||||
|
setPage(1);
|
||||||
|
updateUrlParams({ status: tab.value, page: 1 });
|
||||||
|
}}
|
||||||
className={`px-4 py-2 rounded-xl font-bold text-xs border transition-all cursor-pointer ${
|
className={`px-4 py-2 rounded-xl font-bold text-xs border transition-all cursor-pointer ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-purple-600 text-white border-purple-600 shadow-md shadow-purple-600/20'
|
? 'bg-purple-600 text-white border-purple-600 shadow-md shadow-purple-600/20'
|
||||||
@ -426,12 +522,36 @@ export default function Orders() {
|
|||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-right">
|
<table className="w-full text-right">
|
||||||
<thead className="bg-gray-50 text-gray-500 text-xs uppercase tracking-wider border-b border-gray-200">
|
<thead className="bg-gray-50 text-gray-500 text-xs uppercase tracking-wider border-b border-gray-200 select-none">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="py-4 px-6 font-bold">شماره / کد پیگیری</th>
|
<th className="py-4 px-6 font-bold">شماره / کد پیگیری</th>
|
||||||
<th className="py-4 px-6 font-bold">مشتری</th>
|
<th className="py-4 px-6 font-bold">مشتری</th>
|
||||||
<th className="py-4 px-6 font-bold">مبلغ کل</th>
|
<th
|
||||||
<th className="py-4 px-6 font-bold">تاریخ و ساعت ثبت</th>
|
onClick={() => handleSort('totalAmount')}
|
||||||
|
className="py-4 px-6 font-bold text-gray-700 hover:text-purple-600 cursor-pointer transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span>مبلغ کل</span>
|
||||||
|
{sortBy === 'totalAmount' ? (
|
||||||
|
sortOrder === 'asc' ? <ArrowUp className="w-3.5 h-3.5 text-purple-600" /> : <ArrowDown className="w-3.5 h-3.5 text-purple-600" />
|
||||||
|
) : (
|
||||||
|
<ArrowUpDown className="w-3.5 h-3.5 text-gray-400 opacity-50" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
onClick={() => handleSort('createdAt')}
|
||||||
|
className="py-4 px-6 font-bold text-gray-700 hover:text-purple-600 cursor-pointer transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span>تاریخ و ساعت ثبت</span>
|
||||||
|
{sortBy === 'createdAt' ? (
|
||||||
|
sortOrder === 'asc' ? <ArrowUp className="w-3.5 h-3.5 text-purple-600" /> : <ArrowDown className="w-3.5 h-3.5 text-purple-600" />
|
||||||
|
) : (
|
||||||
|
<ArrowUpDown className="w-3.5 h-3.5 text-gray-400 opacity-50" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
<th className="py-4 px-6 font-bold">اقلام</th>
|
<th className="py-4 px-6 font-bold">اقلام</th>
|
||||||
<th className="py-4 px-6 font-bold">وضعیت سفارش</th>
|
<th className="py-4 px-6 font-bold">وضعیت سفارش</th>
|
||||||
<th className="py-4 px-6 font-bold text-center">عملیات ادمین</th>
|
<th className="py-4 px-6 font-bold text-center">عملیات ادمین</th>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Search, Filter, Box, Plus, Edit2, Trash2, Image as ImageIcon, X, HelpCircle } from 'lucide-react';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { Search, Filter, Box, Plus, Edit2, Trash2, Image as ImageIcon, X, HelpCircle, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import api, { BASE_DOMAIN } from '../services/api';
|
import api, { BASE_DOMAIN } from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
@ -84,10 +85,16 @@ export default function Products() {
|
|||||||
.replace(/^-|-$/g, '');
|
.replace(/^-|-$/g, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filters
|
// URL Query Params Management
|
||||||
const [search, setSearch] = useState('');
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [categoryFilter, setCategoryFilter] = useState('');
|
|
||||||
const [page, setPage] = useState(1);
|
// Filters initialized from URL
|
||||||
|
const [search, setSearch] = useState(() => searchParams.get('search') || '');
|
||||||
|
const [categoryFilter, setCategoryFilter] = useState(() => searchParams.get('category') || '');
|
||||||
|
const [suitableForFilter, setSuitableForFilter] = useState(() => searchParams.get('suitableFor') || '');
|
||||||
|
const [page, setPage] = useState(() => Number(searchParams.get('page')) || 1);
|
||||||
|
const [sortBy, setSortBy] = useState(() => searchParams.get('sortBy') || 'createdAt');
|
||||||
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(() => (searchParams.get('sortOrder') as 'asc' | 'desc') || 'desc');
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const [imageErrors, setImageErrors] = useState<Record<string, boolean>>({});
|
const [imageErrors, setImageErrors] = useState<Record<string, boolean>>({});
|
||||||
const [mediaImageError, setMediaImageError] = useState(false);
|
const [mediaImageError, setMediaImageError] = useState(false);
|
||||||
@ -99,6 +106,33 @@ export default function Products() {
|
|||||||
const [showSymptomSuggestions, setShowSymptomSuggestions] = useState(false);
|
const [showSymptomSuggestions, setShowSymptomSuggestions] = useState(false);
|
||||||
const limit = 10;
|
const limit = 10;
|
||||||
|
|
||||||
|
// Sync state changes to URL search params
|
||||||
|
const updateUrlParams = useCallback((paramsObj: Record<string, string | number | undefined | null>) => {
|
||||||
|
const current = Object.fromEntries(searchParams.entries());
|
||||||
|
const merged = { ...current, ...paramsObj };
|
||||||
|
|
||||||
|
// Remove empty/default values to keep URL clean
|
||||||
|
const cleaned: Record<string, string> = {};
|
||||||
|
Object.entries(merged).forEach(([key, val]) => {
|
||||||
|
if (val !== undefined && val !== null && String(val).trim() !== '' && !(key === 'page' && String(val) === '1') && !(key === 'sortBy' && String(val) === 'createdAt') && !(key === 'sortOrder' && String(val) === 'desc')) {
|
||||||
|
cleaned[key] = String(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setSearchParams(cleaned, { replace: true });
|
||||||
|
}, [searchParams, setSearchParams]);
|
||||||
|
|
||||||
|
const handleSort = (field: string) => {
|
||||||
|
let newOrder: 'asc' | 'desc' = 'asc';
|
||||||
|
if (sortBy === field) {
|
||||||
|
newOrder = sortOrder === 'asc' ? 'desc' : 'asc';
|
||||||
|
}
|
||||||
|
setSortBy(field);
|
||||||
|
setSortOrder(newOrder);
|
||||||
|
setPage(1);
|
||||||
|
updateUrlParams({ sortBy: field, sortOrder: newOrder, page: 1 });
|
||||||
|
};
|
||||||
|
|
||||||
// Modal State
|
// Modal State
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false);
|
const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false);
|
||||||
@ -142,14 +176,25 @@ export default function Products() {
|
|||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
// Fetch products first
|
setIsLoading(true);
|
||||||
const prodRes = await api.get('/admin/products', { params: { page, limit, search, categoryId: categoryFilter } });
|
// Fetch products with full query params
|
||||||
|
const prodRes = await api.get('/admin/products', {
|
||||||
|
params: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
search: search || undefined,
|
||||||
|
categoryId: categoryFilter || undefined,
|
||||||
|
suitableFor: suitableForFilter || undefined,
|
||||||
|
sortBy,
|
||||||
|
sortOrder
|
||||||
|
}
|
||||||
|
});
|
||||||
if (prodRes.data?.data) {
|
if (prodRes.data?.data) {
|
||||||
setProducts(prodRes.data.data);
|
setProducts(prodRes.data.data);
|
||||||
setTotalPages(prodRes.data.meta?.lastPage || 1);
|
setTotalPages(prodRes.data.meta?.lastPage || 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch categories separately so it doesn't break products
|
// Fetch categories separately
|
||||||
try {
|
try {
|
||||||
const catRes = await api.get('/admin/categories');
|
const catRes = await api.get('/admin/categories');
|
||||||
if (catRes.data?.data) {
|
if (catRes.data?.data) {
|
||||||
@ -166,12 +211,12 @@ export default function Products() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [page, search, categoryFilter]);
|
}, [page, search, categoryFilter, suitableForFilter, sortBy, sortOrder]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, 500);
|
}, 400);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [fetchData]);
|
}, [fetchData]);
|
||||||
|
|
||||||
@ -248,15 +293,23 @@ export default function Products() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (editingProduct) {
|
if (editingProduct) {
|
||||||
await api.put(`/admin/products/${editingProduct.id}`, finalPayload);
|
const res = await api.put(`/admin/products/${editingProduct.id}`, finalPayload);
|
||||||
|
const updated = res.data?.data || res.data;
|
||||||
|
if (updated && updated.id) {
|
||||||
|
setProducts(prev => prev.map(p => p.id === editingProduct.id ? { ...p, ...updated } : p));
|
||||||
|
}
|
||||||
toast.success('محصول با موفقیت ویرایش شد');
|
toast.success('محصول با موفقیت ویرایش شد');
|
||||||
} else {
|
} else {
|
||||||
await api.post('/admin/products', finalPayload);
|
const res = await api.post('/admin/products', finalPayload);
|
||||||
|
const created = res.data?.data || res.data;
|
||||||
|
if (created && created.id) {
|
||||||
|
setProducts(prev => [created, ...prev]);
|
||||||
|
}
|
||||||
toast.success('محصول با موفقیت ایجاد شد');
|
toast.success('محصول با موفقیت ایجاد شد');
|
||||||
}
|
}
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
setImageErrors({});
|
setImageErrors({});
|
||||||
fetchData();
|
await fetchData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Save failed', error);
|
console.error('Save failed', error);
|
||||||
toast.error('خطا در ذخیره محصول');
|
toast.error('خطا در ذخیره محصول');
|
||||||
@ -297,40 +350,106 @@ export default function Products() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
|
{/* Filters Bar */}
|
||||||
|
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col md:flex-row gap-4">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Search className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
|
<Search className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="جستجو در نام و کد محصول..."
|
placeholder="جستجو حرفهای در نام فارسی، نام انگلیسی (Latin)، کد محصول، بارکد، ترکیبات و علائم..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
|
onChange={(e) => {
|
||||||
className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all"
|
const val = e.target.value;
|
||||||
|
setSearch(val);
|
||||||
|
setPage(1);
|
||||||
|
updateUrlParams({ search: val, page: 1 });
|
||||||
|
}}
|
||||||
|
className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative w-full sm:w-64">
|
<div className="flex flex-wrap sm:flex-nowrap gap-3">
|
||||||
<Filter className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
|
<div className="relative w-full sm:w-56">
|
||||||
<select
|
<Filter className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||||
value={categoryFilter}
|
<select
|
||||||
onChange={(e) => { setCategoryFilter(e.target.value); setPage(1); }}
|
value={categoryFilter}
|
||||||
className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all appearance-none"
|
onChange={(e) => {
|
||||||
>
|
const val = e.target.value;
|
||||||
<option value="">همه دستهبندیها</option>
|
setCategoryFilter(val);
|
||||||
{categories.map(c => <option key={c.id} value={c.id}>{typeof c === 'string' ? c : (c.nameFa || c.name)}</option>)}
|
setPage(1);
|
||||||
</select>
|
updateUrlParams({ category: val, page: 1 });
|
||||||
|
}}
|
||||||
|
className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all appearance-none bg-white text-sm"
|
||||||
|
>
|
||||||
|
<option value="">همه دستهبندیها</option>
|
||||||
|
{categories.map(c => <option key={c.id} value={c.id}>{typeof c === 'string' ? c : (c.nameFa || c.name)}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="relative w-full sm:w-44">
|
||||||
|
<select
|
||||||
|
value={suitableForFilter}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setSuitableForFilter(val);
|
||||||
|
setPage(1);
|
||||||
|
updateUrlParams({ suitableFor: val, page: 1 });
|
||||||
|
}}
|
||||||
|
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all appearance-none bg-white text-sm"
|
||||||
|
>
|
||||||
|
<option value="">همه گونهها</option>
|
||||||
|
<option value="سگ">مخصوص سگ</option>
|
||||||
|
<option value="گربه">مخصوص گربه</option>
|
||||||
|
<option value="سگ و گربه">سگ و گربه</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-right">
|
<table className="w-full text-right">
|
||||||
<thead className="bg-gray-50 border-b border-gray-100">
|
<thead className="bg-gray-50 border-b border-gray-100 select-none">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="py-4 px-6 text-sm font-bold text-gray-500">تصویر</th>
|
<th className="py-4 px-6 text-sm font-bold text-gray-500">تصویر</th>
|
||||||
<th className="py-4 px-6 text-sm font-bold text-gray-500">کد (Art No)</th>
|
<th
|
||||||
<th className="py-4 px-6 text-sm font-bold text-gray-500">نام محصول</th>
|
onClick={() => handleSort('artNo')}
|
||||||
|
className="py-4 px-6 text-sm font-bold text-gray-700 hover:text-purple-600 cursor-pointer transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span>کد (Art No)</span>
|
||||||
|
{sortBy === 'artNo' ? (
|
||||||
|
sortOrder === 'asc' ? <ArrowUp className="w-4 h-4 text-purple-600" /> : <ArrowDown className="w-4 h-4 text-purple-600" />
|
||||||
|
) : (
|
||||||
|
<ArrowUpDown className="w-3.5 h-3.5 text-gray-400 opacity-50" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
onClick={() => handleSort('nameFa')}
|
||||||
|
className="py-4 px-6 text-sm font-bold text-gray-700 hover:text-purple-600 cursor-pointer transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span>نام محصول</span>
|
||||||
|
{sortBy === 'nameFa' ? (
|
||||||
|
sortOrder === 'asc' ? <ArrowUp className="w-4 h-4 text-purple-600" /> : <ArrowDown className="w-4 h-4 text-purple-600" />
|
||||||
|
) : (
|
||||||
|
<ArrowUpDown className="w-3.5 h-3.5 text-gray-400 opacity-50" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
<th className="py-4 px-6 text-sm font-bold text-gray-500">دستهبندی</th>
|
<th className="py-4 px-6 text-sm font-bold text-gray-500">دستهبندی</th>
|
||||||
<th className="py-4 px-6 text-sm font-bold text-gray-500">موجودی / قیمت</th>
|
<th
|
||||||
|
onClick={() => handleSort('priceValue')}
|
||||||
|
className="py-4 px-6 text-sm font-bold text-gray-700 hover:text-purple-600 cursor-pointer transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span>موجودی / قیمت</span>
|
||||||
|
{sortBy === 'priceValue' ? (
|
||||||
|
sortOrder === 'asc' ? <ArrowUp className="w-4 h-4 text-purple-600" /> : <ArrowDown className="w-4 h-4 text-purple-600" />
|
||||||
|
) : (
|
||||||
|
<ArrowUpDown className="w-3.5 h-3.5 text-gray-400 opacity-50" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
<th className="py-4 px-6 text-sm font-bold text-gray-500 w-24">عملیات</th>
|
<th className="py-4 px-6 text-sm font-bold text-gray-500 w-24">عملیات</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -434,7 +553,7 @@ export default function Products() {
|
|||||||
{ id: 'general', label: 'اطلاعات پایه' },
|
{ id: 'general', label: 'اطلاعات پایه' },
|
||||||
{ id: 'pricing', label: 'موجودی و قیمت' },
|
{ id: 'pricing', label: 'موجودی و قیمت' },
|
||||||
{ id: 'seo', label: 'سئو (SEO)' },
|
{ id: 'seo', label: 'سئو (SEO)' },
|
||||||
{ id: 'media', label: 'تصاویر' }
|
{ id: 'media', label: 'رسانه' }
|
||||||
].map(tab => (
|
].map(tab => (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
|
|||||||
@ -36,6 +36,15 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
|
|
||||||
const ogImg = seoSettings?.ogImageUrl || '/assets/images/regenerated_image_1779109861747.png';
|
const ogImg = seoSettings?.ogImageUrl || '/assets/images/regenerated_image_1779109861747.png';
|
||||||
|
|
||||||
|
const isStaging =
|
||||||
|
process.env.NEXT_PUBLIC_APP_ENV === 'staging' ||
|
||||||
|
process.env.NODE_ENV !== 'production' ||
|
||||||
|
(typeof process.env.NEXT_PUBLIC_SITE_URL === 'string' && (
|
||||||
|
process.env.NEXT_PUBLIC_SITE_URL.includes('stage') ||
|
||||||
|
process.env.NEXT_PUBLIC_SITE_URL.includes('test') ||
|
||||||
|
process.env.NEXT_PUBLIC_SITE_URL.includes('dev')
|
||||||
|
));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
metadataBase: new URL(siteUrl),
|
metadataBase: new URL(siteUrl),
|
||||||
title: {
|
title: {
|
||||||
@ -47,6 +56,26 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
authors: [{ name: 'Canina Iran' }],
|
authors: [{ name: 'Canina Iran' }],
|
||||||
creator: 'Canina Iran',
|
creator: 'Canina Iran',
|
||||||
publisher: 'Canina Iran',
|
publisher: 'Canina Iran',
|
||||||
|
robots: isStaging ? {
|
||||||
|
index: false,
|
||||||
|
follow: false,
|
||||||
|
nocache: true,
|
||||||
|
googleBot: {
|
||||||
|
index: false,
|
||||||
|
follow: false,
|
||||||
|
noimageindex: true,
|
||||||
|
}
|
||||||
|
} : {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
googleBot: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
'max-video-preview': -1,
|
||||||
|
'max-image-preview': 'large',
|
||||||
|
'max-snippet': -1,
|
||||||
|
}
|
||||||
|
},
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: '/',
|
canonical: '/',
|
||||||
},
|
},
|
||||||
@ -77,17 +106,6 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
description: descriptionDefault,
|
description: descriptionDefault,
|
||||||
images: [ogImg],
|
images: [ogImg],
|
||||||
},
|
},
|
||||||
robots: {
|
|
||||||
index: true,
|
|
||||||
follow: true,
|
|
||||||
googleBot: {
|
|
||||||
index: true,
|
|
||||||
follow: true,
|
|
||||||
'max-video-preview': -1,
|
|
||||||
'max-image-preview': 'large',
|
|
||||||
'max-snippet': -1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,22 @@
|
|||||||
import { MetadataRoute } from 'next';
|
import { MetadataRoute } from 'next';
|
||||||
|
|
||||||
export default function robots(): MetadataRoute.Robots {
|
export default function robots(): MetadataRoute.Robots {
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://canino-iran.com';
|
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://canina-iran.com';
|
||||||
|
const isStaging =
|
||||||
|
process.env.NEXT_PUBLIC_APP_ENV === 'staging' ||
|
||||||
|
process.env.NODE_ENV !== 'production' ||
|
||||||
|
baseUrl.includes('stage') ||
|
||||||
|
baseUrl.includes('test') ||
|
||||||
|
baseUrl.includes('dev');
|
||||||
|
|
||||||
|
if (isStaging) {
|
||||||
|
return {
|
||||||
|
rules: {
|
||||||
|
userAgent: '*',
|
||||||
|
disallow: '/',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rules: {
|
rules: {
|
||||||
|
|||||||
@ -27,7 +27,8 @@ import {
|
|||||||
Thermometer,
|
Thermometer,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Share2,
|
Share2,
|
||||||
Phone
|
Phone,
|
||||||
|
Download
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
|
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||||
@ -454,8 +455,14 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
|||||||
<Clock className="w-6 h-6" />
|
<Clock className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg sm:text-2xl font-black text-medical-gray-900 italic font-vazir whitespace-nowrap">ماشین حساب هوشمند مصرف</h3>
|
<h3 className="text-lg sm:text-2xl font-black text-medical-gray-900 italic font-vazir whitespace-nowrap">
|
||||||
<p className="text-[11px] sm:text-xs text-medical-gray-400 font-bold font-vazir mt-0.5 sm:mt-1">تخمین دقیق دوز روزانه و ماندگاری هر بسته بر اساس مشخصات پت شما</p>
|
{activePet ? `ماشین حساب هوشمند مصرف برای ${activePet.name}` : 'ماشین حساب هوشمند مصرف'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-[11px] sm:text-xs text-medical-gray-400 font-bold font-vazir mt-0.5 sm:mt-1">
|
||||||
|
{activePet
|
||||||
|
? `تخمین دقیق دوز روزانه و ماندگاری هر بسته بر اساس وزن و سن ${activePet.name}`
|
||||||
|
: 'تخمین دقیق دوز روزانه و ماندگاری هر بسته بر اساس مشخصات پت شما'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -463,7 +470,9 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
|||||||
{/* Manual Slider Inputs */}
|
{/* Manual Slider Inputs */}
|
||||||
<div className="bg-medical-gray-50 rounded-[2rem] p-6 border border-medical-gray-100 space-y-6">
|
<div className="bg-medical-gray-50 rounded-[2rem] p-6 border border-medical-gray-100 space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-black text-medical-gray-500 font-vazir">وزن پت (کیلوگرم)</span>
|
<span className="text-xs font-black text-medical-gray-500 font-vazir">
|
||||||
|
{activePet ? `وزن ${activePet.name} (کیلوگرم)` : 'وزن پت (کیلوگرم)'}
|
||||||
|
</span>
|
||||||
<span className="text-lg font-black text-canina-blue font-vazir">{toPersian(weight)} کگ</span>
|
<span className="text-lg font-black text-canina-blue font-vazir">{toPersian(weight)} کگ</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@ -475,7 +484,9 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<span className="text-[10px] font-black text-medical-gray-400 font-vazir">سن حیوان</span>
|
<span className="text-[10px] font-black text-medical-gray-400 font-vazir">
|
||||||
|
{activePet ? `سن ${activePet.name}` : 'سن حیوان'}
|
||||||
|
</span>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button onClick={() => setPetType("young")} className={`flex-1 py-3 rounded-xl text-xs font-black transition-all border ${petType === "young" ? 'bg-canina-blue border-canina-blue text-white shadow-lg' : 'bg-white border-medical-gray-200 text-medical-gray-400'}`}>جوان</button>
|
<button onClick={() => setPetType("young")} className={`flex-1 py-3 rounded-xl text-xs font-black transition-all border ${petType === "young" ? 'bg-canina-blue border-canina-blue text-white shadow-lg' : 'bg-white border-medical-gray-200 text-medical-gray-400'}`}>جوان</button>
|
||||||
<button onClick={() => setPetType("adult")} className={`flex-1 py-3 rounded-xl text-xs font-black transition-all border ${petType === "adult" ? 'bg-canina-blue border-canina-blue text-white shadow-lg' : 'bg-white border-medical-gray-200 text-medical-gray-400'}`}>بالغ</button>
|
<button onClick={() => setPetType("adult")} className={`flex-1 py-3 rounded-xl text-xs font-black transition-all border ${petType === "adult" ? 'bg-canina-blue border-canina-blue text-white shadow-lg' : 'bg-white border-medical-gray-200 text-medical-gray-400'}`}>بالغ</button>
|
||||||
@ -488,7 +499,9 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
|||||||
<div className="flex items-center justify-between mb-4 opacity-90 border-b border-white/10 pb-3">
|
<div className="flex items-center justify-between mb-4 opacity-90 border-b border-white/10 pb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Sparkles className="w-4 h-4 text-amber-300" />
|
<Sparkles className="w-4 h-4 text-amber-300" />
|
||||||
<span className="text-[10px] font-bold uppercase tracking-widest font-vazir text-white">محاسبهگر دوره درمانی (AI Calculator)</span>
|
<span className="text-[10px] font-bold uppercase tracking-widest font-vazir text-white">
|
||||||
|
{activePet ? `محاسبهگر دوره درمانی برای ${activePet.name}` : 'محاسبهگر دوره درمانی (AI Calculator)'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] bg-white/20 px-2.5 py-0.5 rounded-full font-black">
|
<span className="text-[10px] bg-white/20 px-2.5 py-0.5 rounded-full font-black">
|
||||||
پیشنهاد: {toPersian(itemQuantity)} بسته
|
پیشنهاد: {toPersian(itemQuantity)} بسته
|
||||||
@ -514,7 +527,9 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-[10px] text-white/60 font-bold font-vazir mt-3 leading-relaxed">
|
<p className="text-[10px] text-white/60 font-bold font-vazir mt-3 leading-relaxed">
|
||||||
* دوز دقیق بر اساس وزن پت محاسبه شده است؛ برای ماندگاری کامل دوره، خرید تعداد بستههای پیشنهادی توصیه میشود.
|
{activePet
|
||||||
|
? `* دوز دقیق بر اساس وزن ${activePet.name} (${toPersian(weight)} کگ) محاسبه شده است؛ برای ماندگاری کامل دوره، خرید تعداد بستههای پیشنهادی توصیه میشود.`
|
||||||
|
: '* دوز دقیق بر اساس وزن پت محاسبه شده است؛ برای ماندگاری کامل دوره، خرید تعداد بستههای پیشنهادی توصیه میشود.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -659,6 +674,100 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Multimedia & Scientific Documentation (Video, Podcast & PDF Catalog) */}
|
||||||
|
{(product.videoUrl || product.podcastUrl || product.pdfUrl) && (
|
||||||
|
<section className="bg-white border border-medical-gray-200 rounded-[2.5rem] p-8 md:p-10 shadow-md space-y-8 font-vazir" dir="rtl">
|
||||||
|
<div className="flex items-center gap-4 border-b border-medical-gray-100 pb-6">
|
||||||
|
<div className="w-12 h-12 bg-purple-600 text-white rounded-2xl flex items-center justify-center shadow-lg">
|
||||||
|
<Sparkles className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl md:text-3xl font-black text-medical-gray-900 italic">رسانه و مستندات تخصصی محصول</h3>
|
||||||
|
<p className="text-xs text-medical-gray-400 font-bold mt-1">ویدیو آموزشی، پادکست تحلیل علمی و دفترچه کاتالوگ کارخانه Canina</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-6">
|
||||||
|
{/* Video Player Box */}
|
||||||
|
{product.videoUrl && (
|
||||||
|
<div className="bg-gray-900 rounded-3xl overflow-hidden shadow-xl border border-gray-800 p-4 space-y-3">
|
||||||
|
<div className="flex items-center justify-between px-2 text-white">
|
||||||
|
<span className="text-sm font-bold flex items-center gap-2">
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full bg-red-500 animate-pulse"></span>
|
||||||
|
ویدیو معرفی و راهنمای محصول
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] text-gray-400 font-mono">Video Guide</span>
|
||||||
|
</div>
|
||||||
|
<div className="relative w-full aspect-video rounded-2xl overflow-hidden bg-black flex items-center justify-center">
|
||||||
|
{product.videoUrl.includes('<iframe') ? (
|
||||||
|
<div
|
||||||
|
className="w-full h-full [&>iframe]:w-full [&>iframe]:h-full"
|
||||||
|
dangerouslySetInnerHTML={{ __html: product.videoUrl }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<video
|
||||||
|
src={product.videoUrl}
|
||||||
|
controls
|
||||||
|
playsInline
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
poster={product.image}
|
||||||
|
>
|
||||||
|
مرورگر شما از پخش ویدیو پشتیبانی نمیکند.
|
||||||
|
</video>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Podcast / Audio Player Box */}
|
||||||
|
{product.podcastUrl && (
|
||||||
|
<div className="bg-gradient-to-r from-blue-50 to-indigo-50/60 border border-blue-200 rounded-3xl p-6 space-y-4 shadow-sm">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-canina-blue text-white flex items-center justify-center shadow-md">
|
||||||
|
🎧
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-black text-gray-900">پادکست و بررسی صوتی بالینی مکمل</h4>
|
||||||
|
<p className="text-[11px] text-gray-500 font-bold">توضیحات صوتی دامپزشک درباره اثربخشی و دوز مصرفی</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] bg-canina-blue/10 text-canina-blue px-3 py-1 rounded-full font-bold">پادکست اختصاصی</span>
|
||||||
|
</div>
|
||||||
|
<audio controls className="w-full h-10 rounded-xl accent-canina-blue">
|
||||||
|
<source src={product.podcastUrl} />
|
||||||
|
مرورگر شما از پخش صوت پشتیبانی نمیکند.
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* PDF Catalog Download Box */}
|
||||||
|
{product.pdfUrl && (
|
||||||
|
<div className="bg-medical-gray-50 border border-medical-gray-200 rounded-3xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4 shadow-xs">
|
||||||
|
<div className="flex items-center gap-4 text-center sm:text-right">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-red-100 text-red-600 flex items-center justify-center font-black text-xs shadow-sm shrink-0">
|
||||||
|
PDF
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-base font-black text-gray-900">بروشور و کاتالوگ علمی Canina آلمان</h4>
|
||||||
|
<p className="text-xs text-gray-500 font-medium mt-0.5">شامل مقالات رفرنس، جدول ترکیبات دقیق و سرتیفیکیت رسمی</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={product.pdfUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="bg-canina-blue hover:bg-blue-700 text-white px-6 py-3 rounded-2xl font-black text-xs transition-all shadow-md shadow-canina-blue/20 flex items-center gap-2 shrink-0 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
<span>دانلود مستقیم فایل PDF</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* FAQ Section */}
|
{/* FAQ Section */}
|
||||||
{product.faqs && product.faqs.length > 0 && (
|
{product.faqs && product.faqs.length > 0 && (
|
||||||
<section className="pt-10">
|
<section className="pt-10">
|
||||||
|
|||||||
@ -61,6 +61,9 @@ export interface Product {
|
|||||||
specialist: Specialist;
|
specialist: Specialist;
|
||||||
image: string;
|
image: string;
|
||||||
images?: string[];
|
images?: string[];
|
||||||
|
podcastUrl?: string;
|
||||||
|
videoUrl?: string;
|
||||||
|
pdfUrl?: string;
|
||||||
relatedProducts?: string[];
|
relatedProducts?: string[];
|
||||||
faqs?: FAQ[];
|
faqs?: FAQ[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,9 @@ interface BackendProduct {
|
|||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
images?: string[];
|
images?: string[];
|
||||||
|
podcastUrl?: string;
|
||||||
|
videoUrl?: string;
|
||||||
|
pdfUrl?: string;
|
||||||
productGroup?: string;
|
productGroup?: string;
|
||||||
ingredientList?: Array<{ ingredient: string }>;
|
ingredientList?: Array<{ ingredient: string }>;
|
||||||
ingredients?: string;
|
ingredients?: string;
|
||||||
@ -147,6 +150,27 @@ export class ProductService {
|
|||||||
specialist: safeParse<Product['specialist']>(data.specialist, defaultSpecialist),
|
specialist: safeParse<Product['specialist']>(data.specialist, defaultSpecialist),
|
||||||
|
|
||||||
calculateDosage: local?.calculateDosage,
|
calculateDosage: local?.calculateDosage,
|
||||||
|
podcastUrl: (() => {
|
||||||
|
const apiBase = (process.env.NEXT_PUBLIC_API_URL || '').replace(/\/api$/, '');
|
||||||
|
if (!data.podcastUrl) return undefined;
|
||||||
|
if (data.podcastUrl.startsWith('http://') || data.podcastUrl.startsWith('https://')) return data.podcastUrl;
|
||||||
|
if (data.podcastUrl.startsWith('/uploads/')) return `${apiBase}${data.podcastUrl}`;
|
||||||
|
return data.podcastUrl;
|
||||||
|
})(),
|
||||||
|
videoUrl: (() => {
|
||||||
|
const apiBase = (process.env.NEXT_PUBLIC_API_URL || '').replace(/\/api$/, '');
|
||||||
|
if (!data.videoUrl) return undefined;
|
||||||
|
if (data.videoUrl.startsWith('http://') || data.videoUrl.startsWith('https://')) return data.videoUrl;
|
||||||
|
if (data.videoUrl.startsWith('/uploads/')) return `${apiBase}${data.videoUrl}`;
|
||||||
|
return data.videoUrl;
|
||||||
|
})(),
|
||||||
|
pdfUrl: (() => {
|
||||||
|
const apiBase = (process.env.NEXT_PUBLIC_API_URL || '').replace(/\/api$/, '');
|
||||||
|
if (!data.pdfUrl) return undefined;
|
||||||
|
if (data.pdfUrl.startsWith('http://') || data.pdfUrl.startsWith('https://')) return data.pdfUrl;
|
||||||
|
if (data.pdfUrl.startsWith('/uploads/')) return `${apiBase}${data.pdfUrl}`;
|
||||||
|
return data.pdfUrl;
|
||||||
|
})(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,293 +1,289 @@
|
|||||||
{
|
{
|
||||||
"0": "SettingsService",
|
"0": "AdminService",
|
||||||
"1": "AdminController",
|
"1": "ProductService",
|
||||||
"2": "UsersService",
|
"2": "CmsController",
|
||||||
"3": "ProductService",
|
"3": "app.module.ts",
|
||||||
"4": "admin.module.ts",
|
"4": "CreateReviewDto",
|
||||||
"5": "auth.service.ts",
|
"5": "tickets.controller.ts",
|
||||||
"6": "OrdersService",
|
"6": "UserDashboard.tsx",
|
||||||
"7": "app.module.ts",
|
"7": "Spinner.tsx",
|
||||||
"8": "CmsController",
|
"8": "admin.module.ts",
|
||||||
"9": "tickets.controller.ts",
|
"9": "useCartStore",
|
||||||
"10": "useSettingsStore",
|
"10": "DoctorsService",
|
||||||
"11": "UserDashboard.tsx",
|
"11": "useSettingsStore",
|
||||||
"12": "CreateVideoDto",
|
"12": "adminRoutes.tsx",
|
||||||
"13": "compilerOptions",
|
"13": "PrismaService",
|
||||||
"14": "CreateReviewDto",
|
"14": "PaginationDto",
|
||||||
"15": "ProductsService",
|
"15": "ProductsService",
|
||||||
"16": "PrismaService",
|
"16": "lib/services/api.ts",
|
||||||
"17": "lib/services/api.ts",
|
"17": "CreateVideoDto",
|
||||||
"18": "devDependencies",
|
"18": "src/services/api.ts",
|
||||||
"19": "JwtAuthGuard",
|
"19": "devDependencies",
|
||||||
"20": "BE-001",
|
"20": "BE-001",
|
||||||
"21": "src/services/api.ts",
|
"21": "Roles",
|
||||||
"22": "RedisService",
|
"22": "FE-001",
|
||||||
"23": "WholesaleApplyDto",
|
"23": "ADM-001",
|
||||||
"24": "FE-001",
|
"24": "DB-001",
|
||||||
"25": "ADM-001",
|
"25": "TS-001",
|
||||||
"26": "DB-001",
|
"26": "TEST-001",
|
||||||
"27": "TS-001",
|
"27": "DEVOPS-001",
|
||||||
"28": "TEST-001",
|
"28": "DOC-001",
|
||||||
"29": "DEVOPS-001",
|
"29": "wholesale.controller.ts",
|
||||||
"30": "DOC-001",
|
"30": "main.ts",
|
||||||
"31": "adminRoutes.tsx",
|
"31": "JwtAuthGuard",
|
||||||
"32": "userStore.ts",
|
"32": "ZibalService",
|
||||||
"33": "Spinner.tsx",
|
"33": "راهنمای تست سیستم (Software Testing)",
|
||||||
"34": "PetProfile.tsx",
|
"34": "CategoriesController",
|
||||||
"35": "main.ts",
|
"35": "B2BService",
|
||||||
"36": "B2BService",
|
"36": "What You Must Do When Invoked",
|
||||||
"37": "ZibalService",
|
"37": "BannersService",
|
||||||
"38": "راهنمای تست سیستم (Software Testing)",
|
"38": "UsersService",
|
||||||
"39": "CategoriesController",
|
"39": "What You Must Do When Invoked",
|
||||||
"40": "MediaController",
|
"40": "SslController",
|
||||||
"41": "What You Must Do When Invoked",
|
"41": "toPersian",
|
||||||
"42": "SslController",
|
"42": "IngredientsService",
|
||||||
"43": "BannersService",
|
"43": "AuthService",
|
||||||
"44": "What You Must Do When Invoked",
|
"44": "MediaController",
|
||||||
"45": "UITexts.tsx",
|
"45": "ConfirmModal.tsx",
|
||||||
"46": "IngredientsService",
|
"46": "auth.controller.ts",
|
||||||
"47": ".update",
|
"47": "SmsService",
|
||||||
"48": "HomeClient.tsx",
|
"48": "PrescriptionsService",
|
||||||
"49": "SmartAdvisorService",
|
"49": "SmartAdvisorService",
|
||||||
"50": "TestimonialsService",
|
"50": "TestimonialsService",
|
||||||
"51": "Role & Core Objective",
|
"51": "Role & Core Objective",
|
||||||
"52": "PaginationDto",
|
"52": "ContactService",
|
||||||
"53": "ContactService",
|
"53": "PaymentController",
|
||||||
"54": "compilerOptions",
|
"54": "compilerOptions",
|
||||||
"55": "Reports.tsx",
|
"55": "Media.tsx",
|
||||||
"56": "compilerOptions",
|
"56": "compilerOptions",
|
||||||
"57": "dependencies",
|
"57": "PetsController",
|
||||||
"58": "PrescriptionsService",
|
"58": "WikiController",
|
||||||
"59": "BlogsController",
|
"59": "dependencies",
|
||||||
"60": "Required Review Group Closures",
|
"60": "compilerOptions",
|
||||||
"61": "devDependencies",
|
"61": "HomeClient.tsx",
|
||||||
"62": "Coupons.tsx",
|
"62": "BlogsController",
|
||||||
"63": "Operational Rules & Boundaries",
|
"63": "AuthController",
|
||||||
"64": "Operational Rules & Boundaries",
|
"64": "PetsService",
|
||||||
"65": "WikiController",
|
"65": "Required Review Group Closures",
|
||||||
"66": "PaymentController",
|
"66": "Coupons.tsx",
|
||||||
"67": "pets/pets.controller.ts",
|
"67": "Operational Rules & Boundaries",
|
||||||
"68": "seo.module.ts",
|
"68": "Operational Rules & Boundaries",
|
||||||
"69": "admin.service.ts",
|
"69": "WikiController",
|
||||||
"70": "🏢 AI Software Agency — Master Orchestration Protocol v3",
|
"70": "pets/pets.controller.ts",
|
||||||
"71": "Operational Rules & Boundaries",
|
"71": "PetsController",
|
||||||
"72": "Operational Rules & Boundaries",
|
"72": "seo.module.ts",
|
||||||
"73": "HomeController",
|
"73": "BlogsService",
|
||||||
"74": "scripts",
|
"74": "🏢 AI Software Agency — Master Orchestration Protocol v3",
|
||||||
"75": "Role & Core Objective",
|
"75": "Operational Rules & Boundaries",
|
||||||
"76": ".handleZibalCallback",
|
"76": "Operational Rules & Boundaries",
|
||||||
"77": "zibal.service.ts",
|
"77": "scripts",
|
||||||
"78": "Orders.tsx",
|
"78": "Role & Core Objective",
|
||||||
"79": "devDependencies",
|
"79": "HomeController",
|
||||||
"80": "exclude",
|
"80": "zibal.service.ts",
|
||||||
"81": "seed-products.ts",
|
"81": "devDependencies",
|
||||||
"82": "Reconciled Audit Roles & Assignments",
|
"82": "api",
|
||||||
"83": "20260526145407_init/migration.sql",
|
"83": "Orders.tsx",
|
||||||
"84": "Phase 3.1 — Human Review Preparation and Master Backlog Critique",
|
"84": "devDependencies",
|
||||||
"85": "dependencies",
|
"85": "seed-products.ts",
|
||||||
"86": "SmsService",
|
"86": "Reconciled Audit Roles & Assignments",
|
||||||
"87": "SafeImage.tsx",
|
"87": "20260526145407_init/migration.sql",
|
||||||
"88": "compilerOptions",
|
"88": "Phase 3.1 — Human Review Preparation and Master Backlog Critique",
|
||||||
"89": "scripts",
|
"89": "dependencies",
|
||||||
"90": "AdminService",
|
"90": "VetGallery.tsx",
|
||||||
"91": "Deep Audit Summary Report",
|
"91": "compilerOptions",
|
||||||
"92": "dependencies",
|
"92": "scripts",
|
||||||
"93": "Operational Rules & Boundaries",
|
"93": "Deep Audit Summary Report",
|
||||||
"94": "jest",
|
"94": "dependencies",
|
||||||
"95": "PetsController",
|
"95": "Operational Rules & Boundaries",
|
||||||
"96": "Comprehensive Change Log",
|
"96": "exclude",
|
||||||
"97": "Operational Rules & Boundaries",
|
"97": "jest",
|
||||||
"98": "BlogsController",
|
"98": "WikiService",
|
||||||
"99": "PetsService",
|
"99": "Comprehensive Change Log",
|
||||||
"100": "1. Summary of Integrity Repairs Performed",
|
"100": "Operational Rules & Boundaries",
|
||||||
"101": "Operational Rules & Boundaries",
|
"101": ".findAll",
|
||||||
"102": "Operational Rules & Boundaries",
|
"102": "SmsLogQueryDto",
|
||||||
"103": "Operational Rules & Boundaries",
|
"103": "ZibalCallbackQueryDto",
|
||||||
"104": "AppService",
|
"104": "VerifyOtpDto",
|
||||||
"105": "AdminTransactionFilterDto",
|
"105": "1. Summary of Integrity Repairs Performed",
|
||||||
"106": "Vazirmatn Changelog",
|
"106": "CreateHealthLogDto",
|
||||||
"107": "Vazirmatn Font فونت وزیرمتن",
|
"107": "Operational Rules & Boundaries",
|
||||||
"108": "Operational Rules & Boundaries",
|
"108": "Operational Rules & Boundaries",
|
||||||
"109": "backend/README.md",
|
"109": "Operational Rules & Boundaries",
|
||||||
"110": "Repository Map",
|
"110": "AppService",
|
||||||
"111": "validate_integrity.js",
|
"111": "CreateReminderDto",
|
||||||
"112": "admin-panel/package.json",
|
"112": "@eslint/eslintrc",
|
||||||
"113": "Sahel-Font",
|
"113": "Vazirmatn Changelog",
|
||||||
"114": "Sahel-Font",
|
"114": "Vazirmatn Font فونت وزیرمتن",
|
||||||
"115": "Role & Core Objective",
|
"115": "Operational Rules & Boundaries",
|
||||||
"116": "orchestrate.py",
|
"116": "compilerOptions",
|
||||||
"117": "backend/package.json",
|
"117": "compilerOptions",
|
||||||
"118": "graphify reference: extra exports and benchmark",
|
"118": "backend/README.md",
|
||||||
"119": "Phase 2 Final Quality Gate Summary Report",
|
"119": "eslint-plugin-prettier",
|
||||||
"120": "Task Modifications Log",
|
"120": "Repository Map",
|
||||||
"121": "Install",
|
"121": "validate_integrity.js",
|
||||||
"122": "ErrorBoundary",
|
"122": "admin-panel/package.json",
|
||||||
"123": "application/package.json",
|
"123": "Sahel-Font",
|
||||||
"124": "generate-openapi.js",
|
"124": "Reports.tsx",
|
||||||
"125": "InitiatePaymentDto",
|
"125": "Sahel-Font",
|
||||||
"126": "SmsLogQueryDto",
|
"126": "Role & Core Objective",
|
||||||
"127": "AuthService",
|
"127": "orchestrate.py",
|
||||||
"128": "System Discovery",
|
"128": "backend/package.json",
|
||||||
"129": "Product Requirement Document (PRD)",
|
"129": "RegisterDto",
|
||||||
"130": "videos.controller.ts",
|
"130": "graphify reference: extra exports and benchmark",
|
||||||
"131": "CreateHealthLogDto",
|
"131": "Phase 2 Final Quality Gate Summary Report",
|
||||||
"132": "CreateReminderDto",
|
"132": "Task Modifications Log",
|
||||||
"133": "reviews.controller.ts",
|
"133": "Install",
|
||||||
"134": "Baseline Command Plan & Reconciled Command History",
|
"134": "ErrorBoundary",
|
||||||
"135": "SmsSettingsPage.tsx",
|
"135": "application/package.json",
|
||||||
"136": "ErrorPages.tsx",
|
"136": "generate-openapi.js",
|
||||||
"137": "compilerOptions",
|
"137": "payment.service.ts",
|
||||||
"138": "with-vpn.sh",
|
"138": "InitiatePaymentDto",
|
||||||
"139": "Architecture Specification",
|
"139": "System Discovery",
|
||||||
"140": "Project Health Audit Report",
|
"140": "Product Requirement Document (PRD)",
|
||||||
"141": "nest-cli.json",
|
"141": "AdminLoginDto",
|
||||||
"142": "graphify reference: query, path, explain",
|
"142": "globals",
|
||||||
"143": "Open Questions",
|
"143": "@nestjs/cli",
|
||||||
"144": "Final Phase 2 Audit Closure Report",
|
"144": "Baseline Command Plan & Reconciled Command History",
|
||||||
"145": "Transactions.tsx",
|
"145": "SmsSettingsPage.tsx",
|
||||||
"146": "open-browsers.js",
|
"146": "ErrorPages.tsx",
|
||||||
"147": "start-dev.js",
|
"147": "with-vpn.sh",
|
||||||
"148": "📝 Active Agent Working Scratchpad",
|
"148": "Architecture Specification",
|
||||||
"149": "🔍 Code Health Audit Review (01_auditor)",
|
"149": "Project Health Audit Report",
|
||||||
"150": "paginated-response.schema.ts",
|
"150": "nest-cli.json",
|
||||||
"151": "PetsController",
|
"151": "prettier",
|
||||||
"152": "Vazirmatn Font README",
|
"152": "graphify reference: query, path, explain",
|
||||||
"153": "Omitted File Inspection Report",
|
"153": "Open Questions",
|
||||||
"154": "Phase 3.2 / 3.3 — Implementation Readiness & Architectural Finalization Report",
|
"154": "Final Phase 2 Audit Closure Report",
|
||||||
"155": "Phase 3 Audit Traceability Matrix",
|
"155": "prisma",
|
||||||
"156": "rebuild_honest_ledger.js",
|
"156": "open-browsers.js",
|
||||||
"157": "validate_evidence_grade.js",
|
"157": "start-dev.js",
|
||||||
"158": "api",
|
"158": "📝 Active Agent Working Scratchpad",
|
||||||
"159": "Reviews.tsx",
|
"159": "🔍 Code Health Audit Review (01_auditor)",
|
||||||
"160": "compilerOptions",
|
"160": "paginated-response.schema.ts",
|
||||||
"161": "blog/[slug]/page.tsx",
|
"161": "Vazirmatn Font README",
|
||||||
"162": "WikiService",
|
"162": "Omitted File Inspection Report",
|
||||||
"163": "API Contract Specification",
|
"163": "Phase 3.2 / 3.3 — Implementation Readiness & Architectural Finalization Report",
|
||||||
"164": "⚙️ Backend Technical Review (05_dev_backend)",
|
"164": "Phase 3 Audit Traceability Matrix",
|
||||||
"165": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)",
|
"165": "rebuild_honest_ledger.js",
|
||||||
"166": "🚀 SEO & Content Strategy Review (12_seo_content)",
|
"166": "validate_evidence_grade.js",
|
||||||
"167": "@types/node",
|
"167": "supertest",
|
||||||
"168": "typescript",
|
"168": "blog/[slug]/page.tsx",
|
||||||
"169": "seed-ui-texts.ts",
|
"169": "API Contract Specification",
|
||||||
"170": "seed-wiki.ts",
|
"170": "⚙️ Backend Technical Review (05_dev_backend)",
|
||||||
"171": "update-blog.dto.ts",
|
"171": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)",
|
||||||
"172": "update-home.dto.ts",
|
"172": "🚀 SEO & Content Strategy Review (12_seo_content)",
|
||||||
"173": "update-wiki.dto.ts",
|
"173": "@types/node",
|
||||||
"174": "graphify reference: add a URL and watch a folder",
|
"174": "typescript",
|
||||||
"175": "graphify reference: commit hook and native CLAUDE.md integration",
|
"175": "seed-ui-texts.ts",
|
||||||
"176": "graphify reference: incremental update and cluster-only",
|
"176": "seed-wiki.ts",
|
||||||
"177": "Raw Finding Verification & Disposition Report",
|
"177": "update-blog.dto.ts",
|
||||||
"178": "React + TypeScript + Vite",
|
"178": "update-home.dto.ts",
|
||||||
"179": "Select.tsx",
|
"179": "update-wiki.dto.ts",
|
||||||
"180": "wiki/[slug]/page.tsx",
|
"180": "graphify reference: add a URL and watch a folder",
|
||||||
"181": "application/README.md",
|
"181": "graphify reference: commit hook and native CLAUDE.md integration",
|
||||||
"182": "NetworkBanner.tsx",
|
"182": "graphify reference: incremental update and cluster-only",
|
||||||
"183": "deploy.sh",
|
"183": "Raw Finding Verification & Disposition Report",
|
||||||
"184": "🔒 Security & Performance Review (09_devops_security)",
|
"184": "React + TypeScript + Vite",
|
||||||
"185": "👁️ UX & Persona Interface Review (08_visual_qa)",
|
"185": "Select.tsx",
|
||||||
"186": "@eslint/eslintrc",
|
"186": "wiki/[slug]/page.tsx",
|
||||||
"187": "typescript-eslint",
|
"187": "ts-node",
|
||||||
"188": "20260526160916_add_ui_texts_and_scientific_terms/migration.sql",
|
"188": "application/README.md",
|
||||||
"189": "prisma/scientificTerms.ts",
|
"189": "@types/express",
|
||||||
"190": "seed-blogs.ts",
|
"190": "deploy.sh",
|
||||||
"191": "seed-custom.ts",
|
"191": "🔒 Security & Performance Review (09_devops_security)",
|
||||||
"192": "graphify reference: GitHub clone and cross-repo merge",
|
"192": "👁️ UX & Persona Interface Review (08_visual_qa)",
|
||||||
"193": "graphify reference: transcribe video and audio",
|
"193": "@types/jest",
|
||||||
"194": "Compiler Diagnostic Dispositions",
|
"194": "typescript-eslint",
|
||||||
"195": "Master Task Backlog (Phase 3.3)",
|
"195": "20260526160916_add_ui_texts_and_scientific_terms/migration.sql",
|
||||||
"196": "build_manifest.js",
|
"196": "prisma/scientificTerms.ts",
|
||||||
"197": "generate_classification.js",
|
"197": "seed-blogs.ts",
|
||||||
"198": "generate_evidence.js",
|
"198": "seed-custom.ts",
|
||||||
"199": "generate_ledger.js",
|
"199": "@types/js-yaml",
|
||||||
"200": "generate_manifest.js",
|
"200": "graphify reference: GitHub clone and cross-repo merge",
|
||||||
"201": "sync_honest_manifest.js",
|
"201": "graphify reference: transcribe video and audio",
|
||||||
"202": "sync_manifest.js",
|
"202": "Compiler Diagnostic Dispositions",
|
||||||
"203": "eslint-plugin-prettier",
|
"203": "Master Task Backlog (Phase 3.3)",
|
||||||
"204": "globals",
|
"204": "build_manifest.js",
|
||||||
"205": "tailwindcss",
|
"205": "generate_classification.js",
|
||||||
"206": "@nestjs/cli",
|
"206": "generate_evidence.js",
|
||||||
"207": "FormField.tsx",
|
"207": "generate_ledger.js",
|
||||||
"208": "Input.tsx",
|
"208": "generate_manifest.js",
|
||||||
"209": "Textarea.tsx",
|
"209": "sync_honest_manifest.js",
|
||||||
"210": "admin-panel/tsconfig.json",
|
"210": "sync_manifest.js",
|
||||||
"211": "about/page.tsx",
|
"211": "@types/multer",
|
||||||
"212": "privacy/page.tsx",
|
"212": "@types/passport-jwt",
|
||||||
"213": "next.config.ts",
|
"213": "tailwindcss",
|
||||||
"214": "Shabnam Font README",
|
"214": "MetricsController",
|
||||||
"215": "AGENTS.md",
|
"215": "eslint-config-next",
|
||||||
"216": "rules/graphify.md",
|
"216": "FormField.tsx",
|
||||||
"217": ".agents/workflows/graphify.md",
|
"217": "Input.tsx",
|
||||||
"218": "instructions.md",
|
"218": "Textarea.tsx",
|
||||||
"219": "prettier",
|
"219": "admin-panel/tsconfig.json",
|
||||||
"220": "prisma",
|
"220": "about/page.tsx",
|
||||||
"221": "supertest",
|
"221": "privacy/page.tsx",
|
||||||
"222": "ts-node",
|
"222": "next.config.ts",
|
||||||
"223": "@types/express",
|
"223": "Shabnam Font README",
|
||||||
"224": "@types/jest",
|
"224": "AGENTS.md",
|
||||||
"225": "@types/js-yaml",
|
"225": "rules/graphify.md",
|
||||||
"226": "@types/multer",
|
"226": ".agents/workflows/graphify.md",
|
||||||
"227": "@types/passport-jwt",
|
"227": "instructions.md",
|
||||||
"228": "eslint-config-next",
|
"228": "eslint-plugin-react-hooks",
|
||||||
"229": "eslint-plugin-react-hooks",
|
"229": "eslint-plugin-react-refresh",
|
||||||
"230": "eslint-plugin-react-refresh",
|
"230": "@tailwindcss/postcss",
|
||||||
"231": "ts-loader",
|
"231": "typescript",
|
||||||
"232": "@tailwindcss/postcss",
|
"232": "@testing-library/jest-dom",
|
||||||
"233": "@types/bcrypt",
|
"233": "@testing-library/react",
|
||||||
"234": "@types/supertest",
|
"234": "@types/react",
|
||||||
"235": "blog.entity.ts",
|
"235": "typescript",
|
||||||
"236": "home.entity.ts",
|
"236": "vitest",
|
||||||
"237": "wiki.entity.ts",
|
"237": "axios",
|
||||||
"238": "User Profile Photo",
|
"238": "tailwindcss",
|
||||||
"239": "CLAUDE.md",
|
"240": "ts-loader",
|
||||||
"240": ".claude/CLAUDE.md",
|
"242": "@types/bcrypt",
|
||||||
"241": "extraction-spec.md",
|
"243": "@types/supertest",
|
||||||
"242": "Products Table",
|
"244": "blog.entity.ts",
|
||||||
"243": "Users Table",
|
"245": "home.entity.ts",
|
||||||
"244": "Architectural Audit Findings",
|
"246": "wiki.entity.ts",
|
||||||
"245": "Cross Boundary Dependencies & Backend Architecture Specification",
|
"247": "User Profile Photo",
|
||||||
"246": "Next.js Agent Rules & Brand Guidelines",
|
"248": "CLAUDE.md",
|
||||||
"247": "robots.ts",
|
"249": ".claude/CLAUDE.md",
|
||||||
"248": "application/eslint.config.mjs",
|
"250": "extraction-spec.md",
|
||||||
"249": "postcss.config.mjs",
|
"251": "Products Table",
|
||||||
"250": "vitest.setup.ts",
|
"252": "Users Table",
|
||||||
"251": "backup_db.sh",
|
"253": "Architectural Audit Findings",
|
||||||
"252": "start.sh",
|
"254": "Cross Boundary Dependencies & Backend Architecture Specification",
|
||||||
"253": "reviews/README.md",
|
"255": "Next.js Agent Rules & Brand Guidelines",
|
||||||
"254": "backend/eslint.config.mjs",
|
"256": "robots.ts",
|
||||||
"255": "User Login API",
|
"257": "application/eslint.config.mjs",
|
||||||
"256": "User Logout API",
|
"258": "postcss.config.mjs",
|
||||||
"257": "generate-openapi.d.ts",
|
"259": "vitest.setup.ts",
|
||||||
"258": "ApiOperation",
|
"260": "backup_db.sh",
|
||||||
"259": "typescript",
|
"261": "start.sh",
|
||||||
"260": "@testing-library/jest-dom",
|
"262": "reviews/README.md",
|
||||||
"261": "@testing-library/react",
|
"263": "backend/eslint.config.mjs",
|
||||||
"262": "@types/react",
|
"264": "User Login API",
|
||||||
"263": "typescript",
|
"265": "User Logout API",
|
||||||
"264": "Roles",
|
"266": "generate-openapi.d.ts",
|
||||||
"265": "Get",
|
"267": "Canina Pharma GmbH",
|
||||||
"266": "vitest",
|
"268": "Pets Table",
|
||||||
"267": "axios",
|
"269": "Canina Iran Project Introduction",
|
||||||
"268": "tailwindcss",
|
"270": "Developer Standards and Architecture",
|
||||||
"279": "Canina Pharma GmbH",
|
"271": "Frontend & Admin Architecture Route Map Specification",
|
||||||
"280": "Pets Table",
|
"272": "Project Backlog and Tasks",
|
||||||
"281": "Canina Iran Project Introduction",
|
"273": "eslint.config.js",
|
||||||
"282": "Developer Standards and Architecture",
|
"274": "postcss.config.js",
|
||||||
"283": "Frontend & Admin Architecture Route Map Specification",
|
"275": "admin-panel/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
|
||||||
"284": "Project Backlog and Tasks",
|
"276": "shabnam-font-v5.0.1/CHANGELOG.md",
|
||||||
"285": "eslint.config.js",
|
"277": "tailwind.config.js",
|
||||||
"286": "postcss.config.js",
|
"278": "vite.config.ts",
|
||||||
"287": "admin-panel/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
|
"279": "application/CLAUDE.md",
|
||||||
"288": "shabnam-font-v5.0.1/CHANGELOG.md",
|
"280": "application/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
|
||||||
"289": "tailwind.config.js",
|
"281": "Sahel Font Sample",
|
||||||
"290": "vite.config.ts",
|
"282": "Shabnam Font Changelog",
|
||||||
"291": "application/CLAUDE.md",
|
"283": "Vazirmatn Changelog",
|
||||||
"292": "application/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
|
"284": "vitest.config.ts",
|
||||||
"293": "Sahel Font Sample",
|
"285": "Sahel Font Variable Sample",
|
||||||
"294": "Shabnam Font Changelog",
|
"286": "Shabnam Font Sample",
|
||||||
"295": "Vazirmatn Changelog",
|
"287": "Production Docker Compose",
|
||||||
"296": "vitest.config.ts",
|
"288": "Staging Docker Compose"
|
||||||
"297": "Sahel Font Variable Sample",
|
|
||||||
"298": "Shabnam Font Sample",
|
|
||||||
"299": "Production Docker Compose",
|
|
||||||
"300": "Staging Docker Compose"
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
4529
graphify-out/2026-08-18/.graphify_analysis.json
Normal file
4529
graphify-out/2026-08-18/.graphify_analysis.json
Normal file
File diff suppressed because it is too large
Load Diff
306
graphify-out/2026-08-18/.graphify_labels.json
Normal file
306
graphify-out/2026-08-18/.graphify_labels.json
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
{
|
||||||
|
"0": "admin.service.ts",
|
||||||
|
"1": "ProductService",
|
||||||
|
"2": "CmsController",
|
||||||
|
"3": "app.module.ts",
|
||||||
|
"4": "reviews.controller.ts",
|
||||||
|
"5": "tickets.controller.ts",
|
||||||
|
"6": "UserDashboard.tsx",
|
||||||
|
"7": "Spinner.tsx",
|
||||||
|
"8": "admin.module.ts",
|
||||||
|
"9": "PetProfile.tsx",
|
||||||
|
"10": "userStore.ts",
|
||||||
|
"11": "useSettingsStore",
|
||||||
|
"12": "adminRoutes.tsx",
|
||||||
|
"13": "PrismaService",
|
||||||
|
"14": "PaginationDto",
|
||||||
|
"15": "ProductsService",
|
||||||
|
"16": "lib/services/api.ts",
|
||||||
|
"17": "CreateVideoDto",
|
||||||
|
"18": "src/services/api.ts",
|
||||||
|
"19": "devDependencies",
|
||||||
|
"20": "BE-001",
|
||||||
|
"21": "Roles",
|
||||||
|
"22": "FE-001",
|
||||||
|
"23": "ADM-001",
|
||||||
|
"24": "DB-001",
|
||||||
|
"25": "TS-001",
|
||||||
|
"26": "TEST-001",
|
||||||
|
"27": "DEVOPS-001",
|
||||||
|
"28": "DOC-001",
|
||||||
|
"29": "WholesaleService",
|
||||||
|
"30": "main.ts",
|
||||||
|
"31": "JwtAuthGuard",
|
||||||
|
"32": "ZibalService",
|
||||||
|
"33": "راهنمای تست سیستم (Software Testing)",
|
||||||
|
"34": "CategoriesController",
|
||||||
|
"35": "B2BService",
|
||||||
|
"36": "What You Must Do When Invoked",
|
||||||
|
"37": "BannersService",
|
||||||
|
"38": "UsersController",
|
||||||
|
"39": "What You Must Do When Invoked",
|
||||||
|
"40": "SslController",
|
||||||
|
"41": "SettingsService",
|
||||||
|
"42": "IngredientsService",
|
||||||
|
"43": "RedisService",
|
||||||
|
"44": "MediaController",
|
||||||
|
"45": "auth.module.ts",
|
||||||
|
"46": "auth.service.ts",
|
||||||
|
"47": "OrdersService",
|
||||||
|
"48": "PrescriptionsService",
|
||||||
|
"49": "SmartAdvisorService",
|
||||||
|
"50": "TestimonialsService",
|
||||||
|
"51": "Role & Core Objective",
|
||||||
|
"52": "ContactService",
|
||||||
|
"53": "PaymentController",
|
||||||
|
"54": "compilerOptions",
|
||||||
|
"55": "UITexts.tsx",
|
||||||
|
"56": "compilerOptions",
|
||||||
|
"57": "PetsController",
|
||||||
|
"58": "WikiController",
|
||||||
|
"59": "dependencies",
|
||||||
|
"60": "compilerOptions",
|
||||||
|
"61": "HomeClient.tsx",
|
||||||
|
"62": "BlogsController",
|
||||||
|
"63": "AuthController",
|
||||||
|
"64": "SmsService",
|
||||||
|
"65": "Required Review Group Closures",
|
||||||
|
"66": "Coupons.tsx",
|
||||||
|
"67": "Operational Rules & Boundaries",
|
||||||
|
"68": "Operational Rules & Boundaries",
|
||||||
|
"69": "WikiController",
|
||||||
|
"70": "pets/pets.controller.ts",
|
||||||
|
"71": "PetsController",
|
||||||
|
"72": "seo.module.ts",
|
||||||
|
"73": "UsersService",
|
||||||
|
"74": "🏢 AI Software Agency — Master Orchestration Protocol v3",
|
||||||
|
"75": "Operational Rules & Boundaries",
|
||||||
|
"76": "Operational Rules & Boundaries",
|
||||||
|
"77": "scripts",
|
||||||
|
"78": "Role & Core Objective",
|
||||||
|
"79": "HomeController",
|
||||||
|
"80": "zibal.service.ts",
|
||||||
|
"81": "devDependencies",
|
||||||
|
"82": "api",
|
||||||
|
"83": "Orders.tsx",
|
||||||
|
"84": "devDependencies",
|
||||||
|
"85": "seed-products.ts",
|
||||||
|
"86": "Reconciled Audit Roles & Assignments",
|
||||||
|
"87": "20260526145407_init/migration.sql",
|
||||||
|
"88": "Phase 3.1 — Human Review Preparation and Master Backlog Critique",
|
||||||
|
"89": "dependencies",
|
||||||
|
"90": "SafeImage.tsx",
|
||||||
|
"91": "compilerOptions",
|
||||||
|
"92": "scripts",
|
||||||
|
"93": "Deep Audit Summary Report",
|
||||||
|
"94": "dependencies",
|
||||||
|
"95": "Operational Rules & Boundaries",
|
||||||
|
"96": "exclude",
|
||||||
|
"97": "jest",
|
||||||
|
"98": "ApiOperation",
|
||||||
|
"99": "Comprehensive Change Log",
|
||||||
|
"100": "Operational Rules & Boundaries",
|
||||||
|
"101": "BlogsController",
|
||||||
|
"102": "SmsLogQueryDto",
|
||||||
|
"103": "ZibalCallbackQueryDto",
|
||||||
|
"104": "Body",
|
||||||
|
"105": "1. Summary of Integrity Repairs Performed",
|
||||||
|
"106": "include",
|
||||||
|
"107": "Operational Rules & Boundaries",
|
||||||
|
"108": "Operational Rules & Boundaries",
|
||||||
|
"109": "Operational Rules & Boundaries",
|
||||||
|
"110": "AppService",
|
||||||
|
"111": "AdminController",
|
||||||
|
"112": "AdminService",
|
||||||
|
"113": "Vazirmatn Changelog",
|
||||||
|
"114": "Vazirmatn Font فونت وزیرمتن",
|
||||||
|
"115": "Operational Rules & Boundaries",
|
||||||
|
"116": "compilerOptions",
|
||||||
|
"117": "compilerOptions",
|
||||||
|
"118": "backend/README.md",
|
||||||
|
"119": "ProductDto",
|
||||||
|
"120": "Repository Map",
|
||||||
|
"121": "validate_integrity.js",
|
||||||
|
"122": "admin-panel/package.json",
|
||||||
|
"123": "Sahel-Font",
|
||||||
|
"124": "Reports.tsx",
|
||||||
|
"125": "Sahel-Font",
|
||||||
|
"126": "Role & Core Objective",
|
||||||
|
"127": "orchestrate.py",
|
||||||
|
"128": "backend/package.json",
|
||||||
|
"129": "RegisterDto",
|
||||||
|
"130": "graphify reference: extra exports and benchmark",
|
||||||
|
"131": "Phase 2 Final Quality Gate Summary Report",
|
||||||
|
"132": "Task Modifications Log",
|
||||||
|
"133": "Install",
|
||||||
|
"134": "ErrorBoundary",
|
||||||
|
"135": "application/package.json",
|
||||||
|
"136": "generate-openapi.js",
|
||||||
|
"137": "AdminTransactionFilterDto",
|
||||||
|
"138": "InitiatePaymentDto",
|
||||||
|
"139": "System Discovery",
|
||||||
|
"140": "Product Requirement Document (PRD)",
|
||||||
|
"141": "AdminLoginDto",
|
||||||
|
"142": "AddressDto",
|
||||||
|
"143": "WholesaleApplyDto",
|
||||||
|
"144": "Baseline Command Plan & Reconciled Command History",
|
||||||
|
"145": "SmsSettingsPage.tsx",
|
||||||
|
"146": "ErrorPages.tsx",
|
||||||
|
"147": "with-vpn.sh",
|
||||||
|
"148": "Architecture Specification",
|
||||||
|
"149": "Project Health Audit Report",
|
||||||
|
"150": "nest-cli.json",
|
||||||
|
"151": "Get",
|
||||||
|
"152": "graphify reference: query, path, explain",
|
||||||
|
"153": "Open Questions",
|
||||||
|
"154": "Final Phase 2 Audit Closure Report",
|
||||||
|
"155": "Transactions.tsx",
|
||||||
|
"156": "open-browsers.js",
|
||||||
|
"157": "start-dev.js",
|
||||||
|
"158": "📝 Active Agent Working Scratchpad",
|
||||||
|
"159": "🔍 Code Health Audit Review (01_auditor)",
|
||||||
|
"160": "paginated-response.schema.ts",
|
||||||
|
"161": "Vazirmatn Font README",
|
||||||
|
"162": "Omitted File Inspection Report",
|
||||||
|
"163": "Phase 3.2 / 3.3 — Implementation Readiness & Architectural Finalization Report",
|
||||||
|
"164": "Phase 3 Audit Traceability Matrix",
|
||||||
|
"165": "rebuild_honest_ledger.js",
|
||||||
|
"166": "validate_evidence_grade.js",
|
||||||
|
"167": "Reviews.tsx",
|
||||||
|
"168": "blog/[slug]/page.tsx",
|
||||||
|
"169": "API Contract Specification",
|
||||||
|
"170": "⚙️ Backend Technical Review (05_dev_backend)",
|
||||||
|
"171": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)",
|
||||||
|
"172": "🚀 SEO & Content Strategy Review (12_seo_content)",
|
||||||
|
"173": "@types/node",
|
||||||
|
"174": "typescript",
|
||||||
|
"175": "seed-ui-texts.ts",
|
||||||
|
"176": "seed-wiki.ts",
|
||||||
|
"177": "update-blog.dto.ts",
|
||||||
|
"178": "update-home.dto.ts",
|
||||||
|
"179": "update-wiki.dto.ts",
|
||||||
|
"180": "graphify reference: add a URL and watch a folder",
|
||||||
|
"181": "graphify reference: commit hook and native CLAUDE.md integration",
|
||||||
|
"182": "graphify reference: incremental update and cluster-only",
|
||||||
|
"183": "Raw Finding Verification & Disposition Report",
|
||||||
|
"184": "React + TypeScript + Vite",
|
||||||
|
"185": "Select.tsx",
|
||||||
|
"186": "wiki/[slug]/page.tsx",
|
||||||
|
"187": "NetworkBanner.tsx",
|
||||||
|
"188": "application/README.md",
|
||||||
|
"189": "lib",
|
||||||
|
"190": "deploy.sh",
|
||||||
|
"191": "🔒 Security & Performance Review (09_devops_security)",
|
||||||
|
"192": "👁️ UX & Persona Interface Review (08_visual_qa)",
|
||||||
|
"193": "@eslint/js",
|
||||||
|
"194": "typescript-eslint",
|
||||||
|
"195": "20260526160916_add_ui_texts_and_scientific_terms/migration.sql",
|
||||||
|
"196": "prisma/scientificTerms.ts",
|
||||||
|
"197": "seed-blogs.ts",
|
||||||
|
"198": "seed-custom.ts",
|
||||||
|
"199": "backend/tsconfig.json",
|
||||||
|
"200": "graphify reference: GitHub clone and cross-repo merge",
|
||||||
|
"201": "graphify reference: transcribe video and audio",
|
||||||
|
"202": "Compiler Diagnostic Dispositions",
|
||||||
|
"203": "Master Task Backlog (Phase 3.3)",
|
||||||
|
"204": "build_manifest.js",
|
||||||
|
"205": "generate_classification.js",
|
||||||
|
"206": "generate_evidence.js",
|
||||||
|
"207": "generate_ledger.js",
|
||||||
|
"208": "generate_manifest.js",
|
||||||
|
"209": "sync_honest_manifest.js",
|
||||||
|
"210": "sync_manifest.js",
|
||||||
|
"211": "react-dom",
|
||||||
|
"212": "zustand",
|
||||||
|
"213": "tailwindcss",
|
||||||
|
"214": "MetricsController",
|
||||||
|
"215": "@types/react-dom",
|
||||||
|
"216": "FormField.tsx",
|
||||||
|
"217": "Input.tsx",
|
||||||
|
"218": "Textarea.tsx",
|
||||||
|
"219": "admin-panel/tsconfig.json",
|
||||||
|
"220": "about/page.tsx",
|
||||||
|
"221": "privacy/page.tsx",
|
||||||
|
"222": "next.config.ts",
|
||||||
|
"223": "Shabnam Font README",
|
||||||
|
"224": "AGENTS.md",
|
||||||
|
"225": "rules/graphify.md",
|
||||||
|
"226": ".agents/workflows/graphify.md",
|
||||||
|
"227": "instructions.md",
|
||||||
|
"228": "bcryptjs",
|
||||||
|
"229": "helmet",
|
||||||
|
"230": "js-yaml",
|
||||||
|
"231": "@nestjs/core",
|
||||||
|
"232": "@nestjs/jwt",
|
||||||
|
"233": "@nestjs/swagger",
|
||||||
|
"234": "@nestjs/throttler",
|
||||||
|
"235": "passport-jwt",
|
||||||
|
"236": "@prisma/client",
|
||||||
|
"237": "swagger-ui-express",
|
||||||
|
"238": "@nestjs/schematics",
|
||||||
|
"239": "ts-jest",
|
||||||
|
"240": "ts-loader",
|
||||||
|
"241": "tsconfig-paths",
|
||||||
|
"242": "@types/bcrypt",
|
||||||
|
"243": "@types/supertest",
|
||||||
|
"244": "blog.entity.ts",
|
||||||
|
"245": "home.entity.ts",
|
||||||
|
"246": "wiki.entity.ts",
|
||||||
|
"247": "User Profile Photo",
|
||||||
|
"248": "CLAUDE.md",
|
||||||
|
"249": ".claude/CLAUDE.md",
|
||||||
|
"250": "extraction-spec.md",
|
||||||
|
"251": "Products Table",
|
||||||
|
"252": "Users Table",
|
||||||
|
"253": "Architectural Audit Findings",
|
||||||
|
"254": "Cross Boundary Dependencies & Backend Architecture Specification",
|
||||||
|
"255": "Next.js Agent Rules & Brand Guidelines",
|
||||||
|
"256": "robots.ts",
|
||||||
|
"257": "application/eslint.config.mjs",
|
||||||
|
"258": "postcss.config.mjs",
|
||||||
|
"259": "vitest.setup.ts",
|
||||||
|
"260": "backup_db.sh",
|
||||||
|
"261": "start.sh",
|
||||||
|
"262": "reviews/README.md",
|
||||||
|
"263": "backend/eslint.config.mjs",
|
||||||
|
"264": "User Login API",
|
||||||
|
"265": "User Logout API",
|
||||||
|
"266": "generate-openapi.d.ts",
|
||||||
|
"267": "Canina Pharma GmbH",
|
||||||
|
"268": "Pets Table",
|
||||||
|
"269": "Canina Iran Project Introduction",
|
||||||
|
"270": "Developer Standards and Architecture",
|
||||||
|
"271": "Frontend & Admin Architecture Route Map Specification",
|
||||||
|
"272": "Project Backlog and Tasks",
|
||||||
|
"273": "eslint.config.js",
|
||||||
|
"274": "postcss.config.js",
|
||||||
|
"275": "admin-panel/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
|
||||||
|
"276": "shabnam-font-v5.0.1/CHANGELOG.md",
|
||||||
|
"277": "tailwind.config.js",
|
||||||
|
"278": "vite.config.ts",
|
||||||
|
"279": "application/CLAUDE.md",
|
||||||
|
"280": "application/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
|
||||||
|
"281": "Sahel Font Sample",
|
||||||
|
"282": "Shabnam Font Changelog",
|
||||||
|
"283": "Vazirmatn Changelog",
|
||||||
|
"284": "vitest.config.ts",
|
||||||
|
"285": "Sahel Font Variable Sample",
|
||||||
|
"286": "Shabnam Font Sample",
|
||||||
|
"287": "Production Docker Compose",
|
||||||
|
"288": "Staging Docker Compose",
|
||||||
|
"289": ".updateSettings",
|
||||||
|
"290": "ApiBearerAuth",
|
||||||
|
"291": "ApiOperation",
|
||||||
|
"292": "ApiQuery",
|
||||||
|
"293": "ApiTags",
|
||||||
|
"294": "Body",
|
||||||
|
"295": "Controller",
|
||||||
|
"296": "Delete",
|
||||||
|
"297": "Get",
|
||||||
|
"298": "Param",
|
||||||
|
"299": "Post",
|
||||||
|
"300": "Put",
|
||||||
|
"301": "Query",
|
||||||
|
"302": "UseGuards",
|
||||||
|
"303": "Injectable"
|
||||||
|
}
|
||||||
1
graphify-out/2026-08-18/.graphify_semantic_marker
Normal file
1
graphify-out/2026-08-18/.graphify_semantic_marker
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"output_tokens": 7105}
|
||||||
1125
graphify-out/2026-08-18/GRAPH_REPORT.md
Normal file
1125
graphify-out/2026-08-18/GRAPH_REPORT.md
Normal file
File diff suppressed because it is too large
Load Diff
110657
graphify-out/2026-08-18/graph.json
Normal file
110657
graphify-out/2026-08-18/graph.json
Normal file
File diff suppressed because it is too large
Load Diff
3290
graphify-out/2026-08-18/manifest.json
Normal file
3290
graphify-out/2026-08-18/manifest.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_scripts_deploy_sh", "label": "deploy.sh", "file_type": "code", "source_file": "scripts/deploy.sh", "source_location": "L1", "metadata": {"language": "bash", "kind": "file"}}, {"id": "$graphify-root$_scripts_deploy_sh__entry", "label": "deploy.sh script", "file_type": "code", "source_file": "scripts/deploy.sh", "source_location": "L1", "metadata": {"language": "bash", "kind": "bash_entrypoint"}}, {"id": "$graphify-root$_scripts_deploy_docker_buildkit", "label": "DOCKER_BUILDKIT", "file_type": "code", "source_file": "scripts/deploy.sh", "source_location": "L4", "metadata": {"language": "bash", "kind": "code"}}, {"id": "$graphify-root$_scripts_deploy_compose_docker_cli_build", "label": "COMPOSE_DOCKER_CLI_BUILD", "file_type": "code", "source_file": "scripts/deploy.sh", "source_location": "L5", "metadata": {"language": "bash", "kind": "code"}}], "edges": [{"source": "$graphify-root$_scripts_deploy_sh", "target": "$graphify-root$_scripts_deploy_sh__entry", "relation": "contains", "confidence": "EXTRACTED", "source_file": "scripts/deploy.sh", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_scripts_deploy_sh", "target": "$graphify-root$_scripts_deploy_docker_buildkit", "relation": "defines", "confidence": "EXTRACTED", "source_file": "scripts/deploy.sh", "source_location": "L4", "weight": 1.0}, {"source": "$graphify-root$_scripts_deploy_sh", "target": "$graphify-root$_scripts_deploy_compose_docker_cli_build", "relation": "defines", "confidence": "EXTRACTED", "source_file": "scripts/deploy.sh", "source_location": "L5", "weight": 1.0}], "raw_calls": [{"language": "bash", "callee": "set", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L2"}, {"language": "bash", "callee": "docker", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L8"}, {"language": "bash", "callee": "true", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L8"}, {"language": "bash", "callee": "echo", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L18"}, {"language": "bash", "callee": "mkdir", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L38"}, {"language": "bash", "callee": "git", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L42"}, {"language": "bash", "callee": "cd", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L45"}, {"language": "bash", "callee": "cp", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L55"}, {"language": "bash", "callee": "sed", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L56"}, {"language": "bash", "callee": "sleep", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L76"}], "bash_sources": []}
|
||||||
2
graphify-out/cache/stat-index.json
vendored
2
graphify-out/cache/stat-index.json
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
91288
graphify-out/graph.json
91288
graphify-out/graph.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user