From 0b4a3ed1e0e68c5b725e660006a947b2ded559c6 Mon Sep 17 00:00:00 2001 From: Reza Taghizadeh <95388378+srt207-reza@users.noreply.github.com> Date: Sun, 5 Oct 2025 19:24:20 +0330 Subject: [PATCH] feat: add some feature to opinionSMS , refactor and code optimization --- docs/codebase-structure.md | 47 +++ docs/coding-guidelines.md | 23 ++ docs/environment-configuration.md | 18 + docs/styling-guide.md | 64 +++ docs/version-control.md | 26 ++ src/apps/new-ui/assets/GroupListIcon.jpg | Bin 0 -> 1383 bytes src/apps/new-ui/assets/waiting-img.png | Bin 2026 -> 0 bytes src/apps/new-ui/assets/waiting-img.svg | 9 + .../OpinionSMS/+components/BottomActions.tsx | 17 + .../+components/CreatePanel/index.tsx | 146 +++++++ .../+components/CreatedSurveyCard.tsx | 75 ++++ .../OpinionSMS/+components/EmptyState.tsx | 22 ++ .../OpinionSMS/+components/GaugeCard.tsx | 31 ++ .../OpinionSMS/+components/GaugeChart.tsx | 148 +++++++ .../pages/OpinionSMS/+components/Header.tsx | 13 + .../OpinionSMS/+components/SearchBar.tsx | 35 ++ .../OpinionSMS/+components/SurveyModal.tsx | 121 ++++++ .../pages/OpinionSMS/+components/UserCard.tsx | 16 +- .../new-ui/pages/OpinionSMS/OpinionSMS.css | 3 + src/apps/new-ui/pages/OpinionSMS/index.tsx | 371 ++++++------------ src/apps/new-ui/pages/OpinionSMS/types.ts | 23 ++ .../ReportAll/+components/Accounting.tsx | 4 +- src/components/BackBtn.tsx | 4 +- 23 files changed, 960 insertions(+), 256 deletions(-) create mode 100644 docs/codebase-structure.md create mode 100644 docs/coding-guidelines.md create mode 100644 docs/environment-configuration.md create mode 100644 docs/styling-guide.md create mode 100644 docs/version-control.md create mode 100644 src/apps/new-ui/assets/GroupListIcon.jpg delete mode 100644 src/apps/new-ui/assets/waiting-img.png create mode 100644 src/apps/new-ui/assets/waiting-img.svg create mode 100644 src/apps/new-ui/pages/OpinionSMS/+components/BottomActions.tsx create mode 100644 src/apps/new-ui/pages/OpinionSMS/+components/CreatePanel/index.tsx create mode 100644 src/apps/new-ui/pages/OpinionSMS/+components/CreatedSurveyCard.tsx create mode 100644 src/apps/new-ui/pages/OpinionSMS/+components/EmptyState.tsx create mode 100644 src/apps/new-ui/pages/OpinionSMS/+components/GaugeCard.tsx create mode 100644 src/apps/new-ui/pages/OpinionSMS/+components/GaugeChart.tsx create mode 100644 src/apps/new-ui/pages/OpinionSMS/+components/Header.tsx create mode 100644 src/apps/new-ui/pages/OpinionSMS/+components/SearchBar.tsx create mode 100644 src/apps/new-ui/pages/OpinionSMS/+components/SurveyModal.tsx create mode 100644 src/apps/new-ui/pages/OpinionSMS/OpinionSMS.css create mode 100644 src/apps/new-ui/pages/OpinionSMS/types.ts diff --git a/docs/codebase-structure.md b/docs/codebase-structure.md new file mode 100644 index 00000000..ce70bdcc --- /dev/null +++ b/docs/codebase-structure.md @@ -0,0 +1,47 @@ +## Codebase Structure + +```markdown +## File Structure + +project-name/ +├── public/ +│ ├── index.html +│ ├── firebase-scripts (To load scripts locally) +│ ├── lotties (Lottie files) +│ ├── statics-images +│ ├── .well-known/ +│ │ ├── assetlinks.json (TWA config file) +│ └── ... +├── src/ +│ ├── apps/ +│ ├── components/ +│ ├── assets/ +│ ├── hooks/ +│ ├── pages/ +│ ├── routes/ +│ ├── utils/ +│ ├── App.css +│ ├── App.tsx +│ ├── main.tsx +│ └── ... +├── package.json +└── ... +``` + +## Source Structure + +- `components/`: Reusable presentational components +- `layouts/`: Components that are layout or container or wrapper of other + components +- `hooks/`: React custom hooks +- `contexts/`: React contexts providers +- `pages/`: Project pages same as created routes +- `services/`: React query and api call hooks and functions / back-end relations +- `routes/`: Main structure of project routes +- `data/`: Static data itmes +- `assets/`: Project assests like images, videos, fonts, etc. +- `utils/`: Utility functions and helpers + +--- + +[Go back to Readme](../README.md) diff --git a/docs/coding-guidelines.md b/docs/coding-guidelines.md new file mode 100644 index 00000000..2fa27980 --- /dev/null +++ b/docs/coding-guidelines.md @@ -0,0 +1,23 @@ +## Style Guide + +- Use the Airbnb JavaScript style guide. +- Prefer functional components over class components. + +## Naming Conventions + +- **Pages:** Use kebab-case (e.g., `page-test.tsx`) exactly like the route of the page +- **Files:** Use PascalCase (e.g., `task-list.tsx`) +- **Components:** Use PascalCase (e.g., `TaskList`) +- **Layouts:** Use PascalCase (e.g., `TaskList`) +- **Hooks:** Use camelCase (e.g., `taskName`) +- **Constants:** Use UPPER_SNAKE_CASE (e.g., `API_URL`) + +## Best Practices + +- Write pure functions where possible. +- Keep components small and focused on a single responsibility. +- Use TypeScript for type safety. + +--- + +[Go back to Readme](../README.md) diff --git a/docs/environment-configuration.md b/docs/environment-configuration.md new file mode 100644 index 00000000..d2dc47a4 --- /dev/null +++ b/docs/environment-configuration.md @@ -0,0 +1,18 @@ +## Environment Configuration + +Create a `.env` file in the root directory and follow the variables in [.env.example](../.env.example) + +```bash +# app back-end api services base url +VITE_APP_API_URL= +# the app website address url to load images +VITE_APP_SITE_URL= +# the digital business card back-end api services base url +VITE_CARD_API_URL= +# the pwa vapid public key +VITE_VAPID_PUBLIC_KEY= +``` + +--- + +[Go back to Readme](../README.md) diff --git a/docs/styling-guide.md b/docs/styling-guide.md new file mode 100644 index 00000000..8cbea72b --- /dev/null +++ b/docs/styling-guide.md @@ -0,0 +1,64 @@ +# Styling Guide + +## Overview + +This project uses [Shadcn ui](https://ui.shadcn.com/) and [Tailwind CSS](https://tailwindcss.com/) for styling, +integrated with the [Vite](https://vitejs.dev/) build tool. This guide explains +how to customize the theme fonts and colors in a Tailwind CSS project set up +with Vite. Tailwind CSS allows for easy theme customization through the +`tailwind.config.js` file. + +## Customizing Fonts + +### Changing Default Fonts + +To change the default fonts, modify the `fontFamily` key in the `theme` section +of `tailwind.config.js`: + +```javascript +module.exports = { + theme: { + fontFamily: { + vazirmatn: ['Vazirmatn', 'tahoma', 'sans-serif'], + nastaliq: ['IranNastaliq', 'Vazirmatn', 'tahoma', 'sans-serif'], + boblious: ['Boblious', 'Vazirmatn', 'tahoma', 'sans-serif'], + hayat: ['Hayat', 'Vazirmatn', 'tahoma', 'sans-serif'], + digiMadasi: ['DigiMadasi', 'Vazirmatn', 'tahoma', 'sans-serif'], + }, + }, + plugins: [], +}; +``` + +### Customizing Colors + +Update `tailwind.config.js` to include custom colors: + +```ts +module.exports = { + theme: { + extend: { + colors: { + primary: '....', + secondary: '....', + accent: '....', + // Add more custom colors as needed + }, + }, + }, + plugins: [], +}; +``` + +Apply the custom colors in your CSS or HTML: + +```html +
+ This div has a custom primary background color. +
+ +``` + +--- + +[Go back to Readme](../README.md) diff --git a/docs/version-control.md b/docs/version-control.md new file mode 100644 index 00000000..82e19612 --- /dev/null +++ b/docs/version-control.md @@ -0,0 +1,26 @@ +## Version Control + +## Branching Strategy + +We follow the Gitflow branching strategy: + +- **main**: Production-ready code. +- **test**: Latest development changes. +- **Feature/**: New features. +- **Bugfix/**: Bug fixes. +- **Refactor/**: Refactor an old feature or code. + +## Commit Message Guidelines + +- Use present tense ("Add feature" not "Added feature"). +- Capitalize the first letter of the commit message. +- Use imperative mood ("Move cursor to..." not "Moves cursor to..."). + +### Examples + +- git commit -m "Fix button alignment" +- git commit -m "Add login functionality" + +--- + +[Go back to Readme](../README.md) diff --git a/src/apps/new-ui/assets/GroupListIcon.jpg b/src/apps/new-ui/assets/GroupListIcon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d2ace6957c4538005b079c7901aa61cc424b2eb6 GIT binary patch literal 1383 zcmbu+drVVz6bJC%ZEq=1pcV>DWOP6)g3d+(8A50TR>8*@kAX7-2AdY>T!V-dWOOR; zhs+lh$a3S0x-l>ekjb`|(iSF08Lp_*NNt&du+DqO>kj%@nTt#I*P`c_-(TnCn|#i( z+_yXet9Ec>xd0+SAjbNDthIAZU{4AQZ% z724mj{|7tvQnDAYe_a~jK!mKvBhml|n103+QGUM&eYgH|t*Zq*I?uRvR%}?a9E1BQ zr4jh(74vOI$7DaF5^*+$=;|vFID%%}g$Wb_Qnq;wo`Uua%A5{qtF;~_Z)k*Dmj}F< zm|tyoYT86{jnacxK3DU)mt#N}8DIj6d02+-Vmsqvh18RSayhvFv$Da;;D>(E5iE?M zsMq*yM;9x&ts+5&>HGHSyGYmN(Q)y`>~)7RJR&FJ|*jEUwh4?PAWB!%c0blVAbCS)+zqh>=#K+c(~fiK9$N zH-+O^EIRREf>-%XxPR*uhtj=XprPtVdyGuYxOjr!%C^()of3DPQ3n}H7KUy|p3)5G zWp8BF5*tx!t#rc&_>4SxRwS1rom8bzf(a_T#kwb7qKON^*VN&Kp1rCj&hZT!!#P;w z?xOKUmHl8;cXM>zyig*I4N~(}WiezGg64%M42S#{>p;5^G{Dg$$J1l(?8vN>@#YHQ zEXpU`23EnqXtpnWY}%H5-B^N2R2G0U*%c9M@qlGrN}o(~(`+}7_2^SnoYlplc-#cr zeFmXQ*BU(#z4aoD%5=KfCH-lj(Fd2H_w-`ood`1Qd`BJ5u>d&B)X?>?q<`toY_$47zO1Y_^;Y>+ey-=z0fSVs zZ+Ttp5lI}s=!;=T(nD}F}PPY#~}L+b9}(@V`` z$AInH#eA59@RxHJp7*f_jkZ)x)rMv61n`4#*mXyj;FYfON~F;#wYoJRUDse_>>89S6_vb51g3;LdV0Wd z9JnQuhQef75t`o95YDOu_Oa0Avh(2D_#|#>c1}nHltMs2qZUu0W9ur zhk+;GfkI)D5-gB)Qo0Vch!O~B_1%52ZS#k4tx$r01rX^bP(0!@sz~InvgN|At)Ifl zQy1`=rYuw_T7e~9*Ms}-Sw3qp%>gwL$MN9K+ZRD^Pj|o?%OP&T^gYE0hpQt6&@?`K zKyeBMl0CN|1JAy?2`*gDD-O{(4Y?wlmf32cEbi`v?OQ&9;nCvk0Fmmnp@ZXv`I+j` zyo?Es>%*HbKLvjtJEO=??=9PF8Dp*xuxdrGa&M9(0-+%pn-5bY~VVS{AJHCYRi87>8sXA>` zddNMY#zLLqHig7rt$4#A|TT3 zB4@NnEg}~RY{AX625*1#4dk(O&<1Ib29jDfPeTe+XL}ZY-T65T=VzIRC>!_Ry%I_l zD{vfv>1@xbJhFf9S85EsB*+sLH;l3%E^EJsfbjSSm#>avrq*Is<;j33Be!Q4s2xQ# zqSaiL_Dc~LQLZW$F%M}WAem?`I{}PZH5m}K4TWM66Jc01k&SR$_ad+{tTUz_wigJD zqN#RZp_9wP&MZt!RO+HHPNu(21Z1Q%IC%I35;$x>Y-~R==}fi#c;X!x8!rY{qAXoA z3LUQp?pp!hZT_LPgTZvFV}$H;|iD%8onuFrz7Cu`l+ z*Mk-;HI2MR;d5(0Y^+5q`1)r#G4xPsFyZP z1M8cBS6+AmE?vnZ;j0%gCF}xtiv|R%IV@kc1UrMU{h--$CP!(y30c2p0DQXtiGZ|` zKh*m?x&DEm=#tCw<5nc>lf}g%>|p8$MluNW_|w}Du3CyH!8+r^sk)Gn9oKGP_n zMjb`bmmibROgA-gm#>U8lml9*mD@evi?q3%;XO4osfHlK8MPa85wdE2bfih2(`vrr ztE4M>{j%`47?!dlkn)oIy!Q@)ss_a&kh(Y-6Ocx@_!!jf*0fE z`o0Ij;dxTvcPXi#m-S4*DAt*v|CVSvNY70 + + + + + + + + diff --git a/src/apps/new-ui/pages/OpinionSMS/+components/BottomActions.tsx b/src/apps/new-ui/pages/OpinionSMS/+components/BottomActions.tsx new file mode 100644 index 00000000..2c41f1c2 --- /dev/null +++ b/src/apps/new-ui/pages/OpinionSMS/+components/BottomActions.tsx @@ -0,0 +1,17 @@ +type Props = { onCreate: () => void; onClose: () => void; hidden?: boolean }; + +const BottomActions: React.FC = ({ onCreate, onClose, hidden }) => { + if (hidden) return null; + return ( +
+ + +
+ ); +}; + +export default BottomActions; diff --git a/src/apps/new-ui/pages/OpinionSMS/+components/CreatePanel/index.tsx b/src/apps/new-ui/pages/OpinionSMS/+components/CreatePanel/index.tsx new file mode 100644 index 00000000..ab4c5539 --- /dev/null +++ b/src/apps/new-ui/pages/OpinionSMS/+components/CreatePanel/index.tsx @@ -0,0 +1,146 @@ +import { useMemo } from "react"; +import { CustomerItem, FormItem } from "../../types"; +import UserCard from "../UserCard"; +import searchIcon from "@new-ui/assets/PurchaseSubscription/Search.svg"; +import microphoneIcon from "@new-ui/assets/PurchaseSubscription/microphone.svg"; +import arrowWhiteIcon from "@new-ui/assets/smsKade/ArrowWhite.svg"; +import arrowGrayIcon from "@new-ui/assets/smsKade/ArrowGray.svg"; + +type Props = { + customers: CustomerItem[]; + forms: FormItem[]; + selectedCustomerId: number | null; + selectedFormId: number | null; + onSelectCustomer: (id: number) => void; + onSelectForm: (id: number) => void; + onCancel: () => void; + onSubmit: () => void; + customerSearch: string; + setCustomerSearch: (s: string) => void; +}; + +const CreatePanel: React.FC = ({ + customers, + forms, + selectedCustomerId, + selectedFormId, + onSelectCustomer, + onSelectForm, + onCancel, + onSubmit, + customerSearch, + setCustomerSearch, +}) => { + const filteredCustomers = useMemo(() => { + const term = customerSearch.trim(); + if (!term) return customers; + return customers.filter((c) => c.title.includes(term) || c.description?.includes(term)); + }, [customers, customerSearch]); + + const canAdd = !!(selectedCustomerId && selectedFormId); + + return ( + <> +
+ + انتخاب مشتری + +
+ search icon + microphone icon + setCustomerSearch(e.target.value)} + className="w-full bg-[#7878801F] py-2 rounded-full px-10" + placeholder="جستجو" + /> +
+ +
+
+
+
+ {filteredCustomers.map((user) => ( + onSelectCustomer(user.id)} + /> + ))} +
+
+
+ +
+ + انتخاب فرم سوالات + + +
+
+
+
+ {forms.map((form) => ( + onSelectForm(form.id)} + onView={() => {}} + /> + ))} +
+
+
+ +
+ + +
+ + ); +}; + +export default CreatePanel; diff --git a/src/apps/new-ui/pages/OpinionSMS/+components/CreatedSurveyCard.tsx b/src/apps/new-ui/pages/OpinionSMS/+components/CreatedSurveyCard.tsx new file mode 100644 index 00000000..c2b68e40 --- /dev/null +++ b/src/apps/new-ui/pages/OpinionSMS/+components/CreatedSurveyCard.tsx @@ -0,0 +1,75 @@ +import { CreatedSurvey } from "../types"; // مسیر را تنظیم کن +import GroupListIcon from "@new-ui/assets/GroupListIcon.jpg"; +import FormIcon from "@new-ui/assets/waiting-img.svg"; +import { Share2, X } from "lucide-react"; + +type Props = { + survey: CreatedSurvey; + isPreview?: boolean; + onDelete: (id: string) => void; + onShare: (s: CreatedSurvey) => void; +}; + +const CreatedSurveyCard: React.FC = ({ survey, isPreview, onDelete, onShare }) => { + const createdAtText = new Date(survey.createdAt).toLocaleDateString("fa-IR", { + year: "numeric", + month: "short", + day: "numeric", + }); + + return ( +
+
+
+
+
+ GroupListIcon +
{survey.form.title}
+
+
در انتظار پاسخ
+
تاریخ ایجاد: {createdAtText}
+
+ + form-img +
+ + {/* user card */} +
+ {survey.customer.title +
+

{survey.customer.title}

+

{survey.customer.description}

+
+
+ + {/* action buttons */} +
+ + +
+
+
+ ); +}; + +export default CreatedSurveyCard; diff --git a/src/apps/new-ui/pages/OpinionSMS/+components/EmptyState.tsx b/src/apps/new-ui/pages/OpinionSMS/+components/EmptyState.tsx new file mode 100644 index 00000000..f42dbf28 --- /dev/null +++ b/src/apps/new-ui/pages/OpinionSMS/+components/EmptyState.tsx @@ -0,0 +1,22 @@ +import EmptyListIcon from "@new-ui/assets/empty-list.svg"; +import { Plus } from "lucide-react"; + +type Props = { onCreate: () => void }; + +const EmptyState: React.FC = ({ onCreate }) => { + return ( +
+ empty List Icon +

لیست فرم نظرسنجی خالی می باشد

+ +
+ ); +}; + +export default EmptyState; diff --git a/src/apps/new-ui/pages/OpinionSMS/+components/GaugeCard.tsx b/src/apps/new-ui/pages/OpinionSMS/+components/GaugeCard.tsx new file mode 100644 index 00000000..ce142ab8 --- /dev/null +++ b/src/apps/new-ui/pages/OpinionSMS/+components/GaugeCard.tsx @@ -0,0 +1,31 @@ +import GaugeChart from "../+components/GaugeChart"; // مسیر رو مطابق پروژه تنظیم کن + +type Props = { + totalSurveys: number; + averagePercent: number; +}; + +const GaugeCard: React.FC = ({ totalSurveys, averagePercent }) => { + if (totalSurveys === 0) return null; + return ( +
+ +
+
{`${averagePercent} درصد در ${totalSurveys} نظرسنجی`}
+
میانگین کل امتیاز ها
+
+
+ ); +}; + +export default GaugeCard; diff --git a/src/apps/new-ui/pages/OpinionSMS/+components/GaugeChart.tsx b/src/apps/new-ui/pages/OpinionSMS/+components/GaugeChart.tsx new file mode 100644 index 00000000..3215d70d --- /dev/null +++ b/src/apps/new-ui/pages/OpinionSMS/+components/GaugeChart.tsx @@ -0,0 +1,148 @@ +import { Cell, Pie, PieChart } from "recharts"; + +type ChartEntry = { name: string; value: number; color: string }; + +type Props = { + data?: ChartEntry[]; + value?: number; // 0..100 (percentage) + width?: number; + height?: number; + needleWidthOverride?: number | null; +}; + +const clamp = (v: number, a = 0, b = 100) => Math.max(a, Math.min(b, v)); + +export default function GaugeChart({ + data = [ + { name: "A", value: 30, color: "#FF6B6B" }, + { name: "B", value: 40, color: "#FFD56B" }, + { name: "C", value: 30, color: "#5FD0B8" }, + ], + value = 0, + width = 300, + height = 160, + needleWidthOverride = null, +}: Props) { + const pct = clamp(value, 0, 100); + + const cx = width / 2; + const cy = Math.round(height * 0.95); + const maxOuter = Math.min(width / 2 - 4, height * 0.95 - 4); + const outerR = Math.round(maxOuter * 0.92); + const innerR = Math.round(outerR * 0.55); + const borderThickness = Math.max(8, Math.round(outerR * 0.13)); + + const angleDeg = 180 * (1 - pct / 100); + // const RAD = Math.PI / 180; + // const angRad = (angleDeg - 90) * RAD; + + // const needleLength = (innerR + (outerR - innerR) * 0.98) * 0.6; + + const needleWidth = needleWidthOverride ?? Math.max(10, Math.round(outerR * 0.1)); + + // const perpX = Math.cos(angRad + Math.PI / 2); + // const perpY = Math.sin(angRad + Math.PI / 2); + + // const baseLeftX = cx + (needleWidth / 2) * perpX; + // const baseLeftY = cy + (needleWidth / 2) * perpY; + // const baseRightX = cx - (needleWidth / 2) * perpX; + // const baseRightY = cy - (needleWidth / 2) * perpY; + + // const tipInset = Math.max(2, Math.round(needleWidth * 0.08)); + // const tipAdjX = cx + (needleLength - tipInset) * Math.cos(angRad); + // const tipAdjY = cy + (needleLength - tipInset) * Math.sin(angRad); + + // const tipRadius = Math.max(5, Math.round(needleWidth * 0.45)); + + const hubOuter = Math.max(6, Math.round(needleWidth * 0.85)); + const hubInner = Math.max(4, Math.round(needleWidth * 0.45)); + + const svgHubX = 78; + const svgHubY = 11.5; + const svgHubR = 6; + const scale = hubOuter / svgHubR; + const x0s = svgHubX * scale; + const y0s = svgHubY * scale; + const dx = cx - x0s; + const dy = cy - y0s; + const rotateDeg = angleDeg - 90; + + const semiPath = (r: number) => { + const sx = cx - r; + const sy = cy; + const ex = cx + r; + const ey = cy; + return `M ${sx} ${sy} A ${r} ${r} 0 0 1 ${ex} ${ey}`; + }; + + return ( +
+ + + + + + + {data.map((entry) => ( + + ))} + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/src/apps/new-ui/pages/OpinionSMS/+components/Header.tsx b/src/apps/new-ui/pages/OpinionSMS/+components/Header.tsx new file mode 100644 index 00000000..1dfa4c50 --- /dev/null +++ b/src/apps/new-ui/pages/OpinionSMS/+components/Header.tsx @@ -0,0 +1,13 @@ +const Header: React.FC<{ title?: string }> = ({ title = "فرم نظرسنجی" }) => { + return ( +
+
+
+ {title} +
+
+
+ ); +}; + +export default Header; diff --git a/src/apps/new-ui/pages/OpinionSMS/+components/SearchBar.tsx b/src/apps/new-ui/pages/OpinionSMS/+components/SearchBar.tsx new file mode 100644 index 00000000..17dea3e1 --- /dev/null +++ b/src/apps/new-ui/pages/OpinionSMS/+components/SearchBar.tsx @@ -0,0 +1,35 @@ +import microphoneIcon from "@new-ui/assets/PurchaseSubscription/microphone.svg"; +import searchIcon from "@new-ui/assets/PurchaseSubscription/Search.svg"; + +type Props = { + value: string; + onChange: (v: string) => void; + placeholder?: string; + className?: string; +}; + +const SearchBar: React.FC = ({ value, onChange, placeholder = "جستجو", className = "" }) => { + return ( +
+ search icon + microphone icon + onChange(e.target.value)} + className="w-full bg-[#F4F2F8] py-3 rounded-full px-10 placeholder:text-[#9B9B9B]" + /> +
+ ); +}; + +export default SearchBar; diff --git a/src/apps/new-ui/pages/OpinionSMS/+components/SurveyModal.tsx b/src/apps/new-ui/pages/OpinionSMS/+components/SurveyModal.tsx new file mode 100644 index 00000000..4f87f7ec --- /dev/null +++ b/src/apps/new-ui/pages/OpinionSMS/+components/SurveyModal.tsx @@ -0,0 +1,121 @@ +import BackBtn from "@/components/BackBtn"; + +type Question = { + id: number; + title: string; + choices?: string[]; +}; + +type SurveyModalProps = { + open: boolean; + onClose: () => void; + questions?: Question[]; +}; + +const defaultQuestions: Question[] = [ + { id: 1, title: "کیفیت خدمات ارائه شده چقدر بود ؟" }, + { + id: 2, + title: "رفتار و برخورد آرایشگر چگونه بود ؟", + choices: ["بسیار خوب", "خوب", "معمولی", "بد"], + }, + { id: 3, title: "میزان تمیزی و بهداشت وسایل چقدر بود تا خط دوم برسد ؟" }, + { id: 4, title: "آیا وقت دهی به موقع انجام شد ؟" }, + { id: 5, title: "آرایشگر چقدر به سلیقه شما اهمیت داد ؟" }, + { id: 6, title: "رضایت شما از مدل نهایی چقدر بود ؟" }, + { id: 7, title: "رضایت شما از مدل نهایی چقدر بود ؟" }, + { id: 8, title: "رضایت شما از مدل نهایی چقدر بود ؟" }, + { id: 9, title: "رضایت شما از مدل نهایی چقدر بود ؟" }, +]; + +export default function SurveyModal({ open, onClose, questions = defaultQuestions }: SurveyModalProps) { + if (!open) return null; + + return ( +
+
+ +
+ {/* header */} + +
+

سوالات فرم شماره 1

+
+ +
+
+ +
+ + {/* فرم انتخابی — کدی که شما فرستادید با کمی تغییر برای سازگاری */} +
+ {/* top gradient (fixed relative to container) */} + + +
+
+ + +
+
+
+
+
+ ); +} diff --git a/src/apps/new-ui/pages/OpinionSMS/+components/UserCard.tsx b/src/apps/new-ui/pages/OpinionSMS/+components/UserCard.tsx index 4ea6ab1d..20097422 100644 --- a/src/apps/new-ui/pages/OpinionSMS/+components/UserCard.tsx +++ b/src/apps/new-ui/pages/OpinionSMS/+components/UserCard.tsx @@ -1,18 +1,22 @@ -import React from "react"; +import { useState } from "react"; import tick from "@new-ui/assets/appointment/tick.svg"; import emptyTick from "@new-ui/assets/appointment/emptyTick.svg"; import { Eye } from "lucide-react"; +import SurveyModal from "./SurveyModal"; interface Props { - img: string; + img: string | undefined; title: string; - description: string; + description: string | undefined; checked: boolean; + questions?: any; onClick?: () => void; onView?: () => void; } -const UserCard: React.FC = ({ img, title, checked, description, onClick, onView }) => { +const UserCard: React.FC = ({ img, title, questions, checked, description, onClick, onView }) => { + const [showModal, setShowModal] = useState(false); + return (
= ({ img, title, checked, description, onClick, }} >
- {title + {title

{title}

= ({ img, title, checked, description, onClick, onClick={(e) => { e.stopPropagation(); onView && onView(); + setShowModal(!showModal); }} className="flex items-center gap-[6px] px-2 ml-2 border-2 border-[#AA00FF] bg-white text-[#AA00FF] text-lg rounded-full" type="button" > دیدن + setShowModal(false)} questions={questions} /> )} {checked ? tick : emptyTick} diff --git a/src/apps/new-ui/pages/OpinionSMS/OpinionSMS.css b/src/apps/new-ui/pages/OpinionSMS/OpinionSMS.css new file mode 100644 index 00000000..5b26b977 --- /dev/null +++ b/src/apps/new-ui/pages/OpinionSMS/OpinionSMS.css @@ -0,0 +1,3 @@ +.recharts-wrapper svg.recharts-surface { + overflow: visible !important; +} \ No newline at end of file diff --git a/src/apps/new-ui/pages/OpinionSMS/index.tsx b/src/apps/new-ui/pages/OpinionSMS/index.tsx index d50f4602..d802769e 100644 --- a/src/apps/new-ui/pages/OpinionSMS/index.tsx +++ b/src/apps/new-ui/pages/OpinionSMS/index.tsx @@ -1,15 +1,17 @@ -import { useEffect, useMemo, useState } from "react"; -import EmptyListIcon from "@new-ui/assets/empty-list.svg"; -import { Plus } from "lucide-react"; -import microphoneIcon from "@new-ui/assets/PurchaseSubscription/microphone.svg"; -import searchIcon from "@new-ui/assets/PurchaseSubscription/Search.svg"; -import UserCard from "./+components/UserCard"; +import { useMemo, useState } from "react"; +import { CustomerItem, FormItem, CreatedSurvey } from "./types"; +import Header from "./+components/Header"; +import SearchBar from "./+components/SearchBar"; +import GaugeCard from "./+components/GaugeCard"; +import EmptyState from "./+components/EmptyState"; +import CreatedSurveyCard from "./+components/CreatedSurveyCard"; +import CreatePanel from "./+components/CreatePanel"; +import BottomActions from "./+components/BottomActions"; import profilePicture from "@new-ui/assets/Profile pic.png"; -import arrowWhiteIcon from "@new-ui/assets/smsKade/ArrowWhite.svg"; -import arrowGrayIcon from "@new-ui/assets/smsKade/ArrowGray.svg"; -import waitingIcon from "@new-ui/assets/waiting-img.png"; +import waitingIcon from "@new-ui/assets/waiting-img.svg"; +import "./OpinionSMS.css"; -const opinionSMSListItemsTest = [ +const opinionSMSListItemsTest: CustomerItem[] = [ { id: 1, img: profilePicture, checked: false, title: "1 پریسا آذری", description: "09121234567" }, { id: 2, img: profilePicture, checked: false, title: "2 پریسا آذری", description: "09121234567" }, { id: 3, img: profilePicture, checked: false, title: "پریسا آذری", description: "09121234567" }, @@ -19,7 +21,7 @@ const opinionSMSListItemsTest = [ { id: 7, img: profilePicture, checked: false, title: "پریسا آذری", description: "09121234567" }, ]; -const opinionSMSFormsListItemsTest = [ +const opinionSMSFormsListItemsTest: FormItem[] = [ { id: 1, img: waitingIcon, @@ -27,273 +29,146 @@ const opinionSMSFormsListItemsTest = [ title: "فرم شماره 1", description: "سوالات استاندارد", questions: [ - { title: "رفتار و برخورد آرایشگر چگونه بود ؟", list: ["بسیار خوب", "خوب", "معمولی", "بد"] }, - { title: "میزان تمیزی و بهداشت وسایل چقدر بود تا خط دوم برسد ؟", list: [] }, - { title: "آیا وقت دهی به موقع انجام شد ؟", list: [] }, - { title: "آیا وقت دهی به موقع انجام شد ؟", list: [] }, - { title: "آیا وقت دهی به موقع انجام شد ؟", list: [] }, - { title: "آیا وقت دهی به موقع انجام شد ؟", list: [] }, - ], - }, - { - id: 2, - img: waitingIcon, - checked: false, - title: "فرم شماره 2", - description: "سوالات تخصصی ترمیم", - questions: [ - { title: "رفتار و برخورد آرایشگر چگونه بود ؟", list: ["بسیار خوب", "خوب", "معمولی", "بد"] }, - { title: "میزان تمیزی و بهداشت وسایل چقدر بود تا خط دوم برسد ؟", list: [] }, - { title: "آیا وقت دهی به موقع انجام شد ؟", list: [] }, - { title: "آیا وقت دهی به موقع انجام شد ؟", list: [] }, - { title: "آیا وقت دهی به موقع انجام شد ؟", list: [] }, - { title: "آیا وقت دهی به موقع انجام شد ؟", list: [] }, - ], - }, - { - id: 3, - img: waitingIcon, - checked: false, - title: "فرم شماره 3", - description: "سوالات تخصصی ترمیم", - questions: [ - { title: "رفتار و برخورد آرایشگر چگونه بود ؟", list: ["بسیار خوب", "خوب", "معمولی", "بد"] }, - { title: "میزان تمیزی و بهداشت وسایل چقدر بود تا خط دوم برسد ؟", list: [] }, - { title: "آیا وقت دهی به موقع انجام شد ؟", list: [] }, - { title: "آیا وقت دهی به موقع انجام شد ؟", list: [] }, - { title: "آیا وقت دهی به موقع انجام شد ؟", list: [] }, - { title: "آیا وقت دهی به موقع انجام شد ؟", list: [] }, - ], - }, - { - id: 4, - img: waitingIcon, - checked: false, - title: "فرم شماره 4", - description: "سوالات تخصصی ترمیم", - questions: [ - { title: "رفتار و برخورد آرایشگر چگونه بود ؟", list: ["بسیار خوب", "خوب", "معمولی", "بد"] }, - { title: "میزان تمیزی و بهداشت وسایل چقدر بود تا خط دوم برسد ؟", list: [] }, - { title: "آیا وقت دهی به موقع انجام شد ؟", list: [] }, - { title: "آیا وقت دهی به موقع انجام شد ؟", list: [] }, - { title: "آیا وقت دهی به موقع انجام شد ؟", list: [] }, - { title: "آیا وقت دهی به موقع انجام شد ؟", list: [] }, + { id: 1, title: "رفتار و برخورد آرایشگر چگونه بود ؟", choices: ["بسیار خوب", "خوب", "معمولی", "بد"] }, + { id: 2, title: "میزان تمیزی و بهداشت وسایل چقدر بود تا خط دوم برسد ؟" }, + { id: 3, title: "آیا وقت دهی به موقع انجام شد ؟" }, ], }, + { id: 2, img: waitingIcon, checked: false, title: "فرم شماره 2", description: "سوالات تخصصی ترمیم" }, + { id: 3, img: waitingIcon, checked: false, title: "فرم شماره 3", description: "سوالات تخصصی ترمیم" }, + { id: 4, img: waitingIcon, checked: false, title: "فرم شماره 4", description: "سوالات تخصصی ترمیم" }, ]; const OpinionSMS: React.FC = () => { - const [opinionSMSListState, setOpinionSMSListState] = useState(opinionSMSListItemsTest); - const [opinionSMSFormsState] = useState(opinionSMSFormsListItemsTest); + const [customers] = useState(opinionSMSListItemsTest); + const [forms] = useState(opinionSMSFormsListItemsTest); - // selected ids (کنترل انتخاب‌ها در والد) + // selection states const [selectedCustomerId, setSelectedCustomerId] = useState(null); const [selectedFormId, setSelectedFormId] = useState(null); + const [isShowCreateList, setIsShowCreateList] = useState(false); + + // created surveys + const [createdSurveys, setCreatedSurveys] = useState([]); + const [showCreatedPreviewId, setShowCreatedPreviewId] = useState(null); + + // searches + const [searchMain, setSearchMain] = useState(""); + const [customerSearch, setCustomerSearch] = useState(""); const selectedCustomer = useMemo( - () => opinionSMSListState.find((c) => c.id === selectedCustomerId) || null, - [opinionSMSListState, selectedCustomerId] - ); - const selectedForm = useMemo( - () => opinionSMSFormsState.find((f) => f.id === selectedFormId) || null, - [opinionSMSFormsState, selectedFormId] + () => customers.find((c) => c.id === selectedCustomerId) || null, + [customers, selectedCustomerId] ); + const selectedForm = useMemo(() => forms.find((f) => f.id === selectedFormId) || null, [forms, selectedFormId]); - const [isShowCreateList, setIsShowCreateList] = useState(false); - const [searchTerm, setSearchTerm] = useState(""); + const totalSurveys = createdSurveys.length; + const averagePercent = 0; // placeholder - useEffect(() => {}, [selectedCustomerId, selectedFormId, isShowCreateList]); + const submitSurvey = () => { + if (!(selectedCustomer && selectedForm)) return; - // search filter (title and description) - const filteredCustomers = useMemo(() => { - const term = searchTerm.trim(); - if (!term) return opinionSMSListState; - return opinionSMSListState.filter((c) => c.title.includes(term) || c.description.includes(term)); - }, [opinionSMSListState, searchTerm]); - - const canAdd = !!(selectedCustomerId && selectedFormId); - - function handleCustomerClick(user: { id: number }) { - if (selectedCustomerId === user.id) { - setSelectedCustomerId(null); - } else { - setSelectedCustomerId(user.id); - } - } - - function handleFormClick(form: { id: number }) { - if (selectedFormId === form.id) { - setSelectedFormId(null); - } else { - setSelectedFormId(form.id); - } - } - - function handleViewForm(form: any) { - console.log("view form", form); - } - - function submitSurvey() { - if (!canAdd) return; - const payload = { + const payload: CreatedSurvey = { + id: `${Date.now()}`, customer: selectedCustomer, form: selectedForm, createdAt: new Date().toISOString(), }; - console.log("submitSurvey payload:", payload); + // reset selection and hide panel first setSelectedCustomerId(null); setSelectedFormId(null); setIsShowCreateList(false); - } + + setTimeout(() => { + setCreatedSurveys((p) => [payload, ...p]); + setShowCreatedPreviewId(payload.id); + }, 0); + }; + + const handleDeleteCreatedSurvey = (id: string) => { + setCreatedSurveys((p) => p.filter((s) => s.id !== id)); + if (showCreatedPreviewId === id) setShowCreatedPreviewId(null); + }; + + const handleShareCreatedSurvey = (s: CreatedSurvey) => { + const text = `فرم نظرسنجی برای ${s.customer.title} - ${s.form.title}`; + if ((navigator as any).share) { + (navigator as any).share({ title: "فرم نظرسنجی", text }).catch(() => {}); + } else { + console.log("share:", text); + } + }; + + const filteredCreatedSurveys = useMemo(() => { + const t = searchMain.trim(); + if (!t) return createdSurveys; + return createdSurveys.filter( + (s) => + s.form.title.includes(t) || + s.form.description?.includes(t) || + s.customer.title.includes(t) || + s.customer.description?.includes(t) + ); + }, [createdSurveys, searchMain]); return ( <> -

-
-
- فرم نظرسنجی -
+
+ {/* Gauge + Main Search */} + {!isShowCreateList && totalSurveys > 0 && ( +
+ +
-
-
- {!opinionSMSListState.length || - (!isShowCreateList && ( -
- empty List Icon -

- لیست فرم نظرسنجی خالی می باشد -

- + )} + + {/* Created Surveys */} + {!isShowCreateList && filteredCreatedSurveys.length > 0 ? ( +
+ {filteredCreatedSurveys.map((s) => ( +
+
))} -
+
+ ) : ( + !isShowCreateList && setIsShowCreateList(true)} /> + )} + + {/* Create panel */} {isShowCreateList && ( - <> - {/* =====select customer======= */} -
- - انتخاب مشتری - -
- search icon - microphone icon - setSearchTerm(e.target.value)} - className="w-full bg-[#7878801F] py-2 rounded-full px-10" - /> -
+ setSelectedCustomerId((p) => (p === id ? null : id))} + onSelectForm={(id) => setSelectedFormId((p) => (p === id ? null : id))} + onCancel={() => { + setIsShowCreateList(false); + setSelectedCustomerId(null); + setSelectedFormId(null); + }} + onSubmit={submitSurvey} + customerSearch={customerSearch} + setCustomerSearch={setCustomerSearch} + /> + )} -
- {/* top gradient (fixed relative to container) */} - - - {/* =====select questions form list======= */} -
- - انتخاب فرم سوالات - - -
- {/* top gradient (fixed relative to container) */} - - - {/* =======buttons========= */} -
- - -
- + {/* Bottom actions */} + {filteredCreatedSurveys.length != 0 && ( + setIsShowCreateList(true)} + onClose={() => console.log("close")} + hidden={isShowCreateList} + /> )} ); diff --git a/src/apps/new-ui/pages/OpinionSMS/types.ts b/src/apps/new-ui/pages/OpinionSMS/types.ts new file mode 100644 index 00000000..457378b0 --- /dev/null +++ b/src/apps/new-ui/pages/OpinionSMS/types.ts @@ -0,0 +1,23 @@ +export type CustomerItem = { + id: number; + img?: string; + checked?: boolean; + title: string; + description?: string; +}; + +export type FormItem = { + id: number; + img?: string; + checked?: boolean; + title: string; + description?: string; + questions?: { id: number; title: string; choices?: string[] }[]; +}; + +export type CreatedSurvey = { + id: string; + customer: CustomerItem; + form: FormItem; + createdAt: string; +}; diff --git a/src/apps/new-ui/pages/ReportAll/+components/Accounting.tsx b/src/apps/new-ui/pages/ReportAll/+components/Accounting.tsx index 0238e530..bccf326e 100644 --- a/src/apps/new-ui/pages/ReportAll/+components/Accounting.tsx +++ b/src/apps/new-ui/pages/ReportAll/+components/Accounting.tsx @@ -162,7 +162,7 @@ const Accounting: React.FC = () => { const [reportDateRange, setReportDateRange] = useState(1); const [isShowDateRange, setIsShowDateRange] = useState(false); - const { control, handleSubmit } = useForm({ + const { control, } = useForm({ defaultValues: { startDate: "", endDate: "", @@ -186,7 +186,7 @@ const Accounting: React.FC = () => { if (id === 3) setIsShowDateRange(true); }; - const [selectedDates, setSelectedDates] = useState(undefined); + // const [selectedDates, setSelectedDates] = useState(undefined); return (
diff --git a/src/components/BackBtn.tsx b/src/components/BackBtn.tsx index 72967ee5..85723851 100644 --- a/src/components/BackBtn.tsx +++ b/src/components/BackBtn.tsx @@ -4,14 +4,16 @@ import { XIcon } from "lucide-react"; const BackBtn = ({ // label, onClick, + customCalss, }: { label?: string; onClick?: () => void; + customCalss?:string }) => { const navigate = useNavigate(); return (