fix some bugs
This commit is contained in:
1 parent
b8b3aba855
commit
963fd839dc
22 files changed
+570
-33
No files matched your search
+191
-8
@@ -1,11 +1,11 @@
|
||||
Project Root: customerTicket
|
||||
Generated At: 2026-09-10T06:55:10+00:00
|
||||
Python Files: 174
|
||||
Generated At: 2026-09-15T04:54:39+00:00
|
||||
Python Files: 186
|
||||
Django Files: 37
|
||||
JavaScript/TypeScript Files: 17
|
||||
HTML Files: 29
|
||||
Total Files: 648
|
||||
Total Directories: 80
|
||||
JavaScript/TypeScript Files: 20
|
||||
HTML Files: 30
|
||||
Total Files: 821
|
||||
Total Directories: 90
|
||||
|
||||
customerTicket
|
||||
├── apps
|
||||
@@ -34,6 +34,7 @@ customerTicket
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── admin.py
|
||||
│ │ ├── apps.py
|
||||
│ │ ├── delivery.py
|
||||
│ │ ├── models.py
|
||||
│ │ ├── services.py
|
||||
│ │ ├── urls.py
|
||||
@@ -43,6 +44,8 @@ customerTicket
|
||||
│ │ │ ├── commands
|
||||
│ │ │ │ ├── __init__.py
|
||||
│ │ │ │ ├── bootstrap.py
|
||||
│ │ │ │ ├── export_production_seed.py
|
||||
│ │ │ │ ├── import_production_seed.py
|
||||
│ │ │ │ └── normalize_persian_data.py
|
||||
│ │ │ └── __init__.py
|
||||
│ │ ├── migrations
|
||||
@@ -51,6 +54,7 @@ customerTicket
|
||||
│ │ │ ├── 0003_sitebranding_developer_credit_html_and_more.py
|
||||
│ │ │ ├── 0004_sitebranding_staff_sidebar_credit_html_and_more.py
|
||||
│ │ │ ├── 0005_sitebranding_company_logo_and_more.py
|
||||
│ │ │ ├── 0006_productionseedstate.py
|
||||
│ │ │ └── __init__.py
|
||||
│ │ ├── templatetags
|
||||
│ │ │ ├── __init__.py
|
||||
@@ -62,7 +66,8 @@ customerTicket
|
||||
│ │ ├── iran_locations.py
|
||||
│ │ ├── middleware.py
|
||||
│ │ ├── models.py
|
||||
│ │ └── persian_text.py
|
||||
│ │ ├── persian_text.py
|
||||
│ │ └── production_seed.py
|
||||
│ ├── customers
|
||||
│ │ ├── migrations
|
||||
│ │ │ ├── 0001_initial.py
|
||||
@@ -117,6 +122,8 @@ customerTicket
|
||||
│ │ │ ├── 0007_notificationjob_notif_due_idx_and_more.py
|
||||
│ │ │ ├── 0008_notificationjob_provider_message_id.py
|
||||
│ │ │ ├── 0009_ensure_global_sms_templates.py
|
||||
│ │ │ ├── 0010_use_actual_unit_in_staff_ticket_sms.py
|
||||
│ │ │ ├── 0011_notification_delivery_tracking.py
|
||||
│ │ │ └── __init__.py
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── admin.py
|
||||
@@ -134,6 +141,7 @@ customerTicket
|
||||
│ │ │ ├── 0007_organizationalunit_show_in_customer_form.py
|
||||
│ │ │ ├── 0008_unitmembership_ticket_scope_and_roles.py
|
||||
│ │ │ ├── 0009_alter_staffprofile_sort_order.py
|
||||
│ │ │ ├── 0010_organizationalunit_notification_destinations.py
|
||||
│ │ │ └── __init__.py
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── admin.py
|
||||
@@ -157,6 +165,7 @@ customerTicket
|
||||
│ │ │ ├── 0013_merge_legacy_accounting_unit.py
|
||||
│ │ │ ├── 0014_merge_legacy_warehouse_units.py
|
||||
│ │ │ ├── 0015_seed_customer_category_catalog.py
|
||||
│ │ │ ├── 0016_scope_legacy_public_tickets_to_units.py
|
||||
│ │ │ └── __init__.py
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── admin.py
|
||||
@@ -182,12 +191,20 @@ customerTicket
|
||||
│ ├── env.py
|
||||
│ ├── urls.py
|
||||
│ └── wsgi.py
|
||||
├── design-prototypes
|
||||
│ └── ticket-new-wizard
|
||||
│ ├── index.html
|
||||
│ ├── prototype.js
|
||||
│ ├── styles.css
|
||||
│ └── test_prototype_e2e.py
|
||||
├── docker
|
||||
│ ├── entrypoint.sh
|
||||
│ └── nginx.conf
|
||||
├── docs
|
||||
│ ├── DEPLOYMENT_COOLIFY.md
|
||||
│ ├── ORGANIZATIONAL_TICKETING.md
|
||||
│ ├── ORGANIZATIONAL_TICKETING_GAP_ANALYSIS.md
|
||||
│ ├── PRODUCTION_SEED.md
|
||||
│ ├── QA_PERMISSION_MATRIX.md
|
||||
│ └── THEMING.md
|
||||
├── graphify-out
|
||||
@@ -223,40 +240,69 @@ customerTicket
|
||||
│ │ ├── graph.json
|
||||
│ │ ├── GRAPH_REPORT.md
|
||||
│ │ └── manifest.json
|
||||
│ ├── 2026-09-14
|
||||
│ │ ├── .graphify_analysis.json
|
||||
│ │ ├── .graphify_labels.json
|
||||
│ │ ├── cost.json
|
||||
│ │ ├── graph.json
|
||||
│ │ ├── GRAPH_REPORT.md
|
||||
│ │ └── manifest.json
|
||||
│ ├── cache
|
||||
│ │ ├── ast
|
||||
│ │ │ └── v0.9.56-s2
|
||||
│ │ │ ├── 00aea1c179c20548df2499948796faba373334997bfeddad9e335db138753cc5.json
|
||||
│ │ │ ├── 00afff188288984e240329931ac9e4273f5ab89be582e021e9edbdbead42ebfe.json
|
||||
│ │ │ ├── 0138bfeaa54b14916df0028a970de356913b83e8da2cd5cfc610361887baa9c2.json
|
||||
│ │ │ ├── 01d84090a3b22716edb63a5cf9c0a56cc94c0f5207cdbd5005843f858bbb292f.json
|
||||
│ │ │ ├── 02108c5a5a69113e104cb5a933a331a5de86cbb18203baf5339a714de46c2372.json
|
||||
│ │ │ ├── 027726034309b90d12137f6110b86751f2558dec73d338c93e190b2f9706caa4.json
|
||||
│ │ │ ├── 02a3519f5b471b7083c161d9d9c73ab5b0ee78cf7e2cc4d5be7c2542435aa58e.json
|
||||
│ │ │ ├── 039590a02c184d21a28caec4178b981a824ec4356a225d1ce546cc212d2f550f.json
|
||||
│ │ │ ├── 053337dddf6eb41ca0de06296d382047253aa08d9b91d61fe86923462c4e2dc8.json
|
||||
│ │ │ ├── 058b4e36240cd25d158647c300472baad5e7101ebde0319883abb0edc8c9c74d.json
|
||||
│ │ │ ├── 074e22579bb4a8d9c4f0c12cdf3cee2aa1dbc9e91778fdbbbbe22ce014511f47.json
|
||||
│ │ │ ├── 07e174cf218887aa0f8747199874faf6bc8af9347e43ff9b5a2073c089637f6f.json
|
||||
│ │ │ ├── 08ff4daae812563107028890c2ae7257bb5f9a376a978244c814b7a4c1d10a98.json
|
||||
│ │ │ ├── 091194a653ec9ff59e50d6d6121d12c0b0238b3f5cbb0467133a224b7eda59c8.json
|
||||
│ │ │ ├── 0aa59f78fa092e41298775a4db40136ea63654d45e3ae835903c4e3dbd0578d4.json
|
||||
│ │ │ ├── 0aafac1c4b289aeef9a6251e3a12fbf1710ae576dbd647d155aa3981008d50b7.json
|
||||
│ │ │ ├── 0bc6d4b595803438408992e7a267aabdae34a6e30bcb947a90307114918928fb.json
|
||||
│ │ │ ├── 0d5f84af41009fdecae2a46d4ad5fab38a79c1a5dde262e9c7f8d35707a35092.json
|
||||
│ │ │ ├── 0d837f2122843751eeb3e945c9c56992b5c410b54ca0dbceccf5413022601d27.json
|
||||
│ │ │ ├── 0ef31d053655ee64599a0246f26c8eb61f27654ae1ffbd41b87500438fc9b06a.json
|
||||
│ │ │ ├── 0fd49b978854a13276bcd9479e5d287e54f19e235d799825f049400153a78c6f.json
|
||||
│ │ │ ├── 11d2b71feb1480622fb18ee3795cf070421b146e4273b5016fad81e7cc8e2c7e.json
|
||||
│ │ │ ├── 12db2ab20d1e0be8080b5edd6cc40cacfdff350dd585126a0e0ce144073ad12f.json
|
||||
│ │ │ ├── 12f95828a2c1818402459627e36481199020e31faa280d214f81700fb9ad520d.json
|
||||
│ │ │ ├── 134a89bb3cc350a74b9b750cc913b6dec440dc2f7ee7bf1eb224793ac3a07544.json
|
||||
│ │ │ ├── 143ae363276b4a1db64805529e618db71c204aa56bc8b54761693a9a2060336c.json
|
||||
│ │ │ ├── 15539048155c760644fc70dd1fbff3855ceedf11bc6ef7b270d1c7280cb4df48.json
|
||||
│ │ │ ├── 157a547bf11b52f36b8ae31334708f612d0d615ad75a9d7e42076806661217d7.json
|
||||
│ │ │ ├── 161e1e32c695094d9d3ceb2a867fa497be0aaef0db27bdfdf94489c6f306e82b.json
|
||||
│ │ │ ├── 16f67769b1d3e30239a9537cd98689a1f4448c60b2cc8bfdf8468ac9dc712ba6.json
|
||||
│ │ │ ├── 178afffc3f3531ee7ffdae18dcbd5505108e5c07892b776a9d8a3413fa39c56d.json
|
||||
│ │ │ ├── 17ab5521ea572ea85443db030bd5085f1443c20b20da9cbf808f21ad695eb319.json
|
||||
│ │ │ ├── 17ff6234af168a40b807e5226bfdb41cf2011a7134c763aba3c69428b6879d66.json
|
||||
│ │ │ ├── 18ba25713068baf53992eb4ea94f95212ecaeb931fdcf6769970fc2fd19b2f93.json
|
||||
│ │ │ ├── 19ed1276256eb4d44b9cea2dddac4de3801d67a08cf381a55466e82ee6f2bbc6.json
|
||||
│ │ │ ├── 1b9e0fc2a347605b7d5ae84c15a0dc71fca5af90fa461763085e622f4c5877ec.json
|
||||
│ │ │ ├── 1bfc839bf502b97d0b70b0bacc9781cae7511d27d078ed6ac6978fd438833dd1.json
|
||||
│ │ │ ├── 1d6b17be6b2c0e7d25b4622b4a8c09ab705cecf9db09e2f86c35a4a564485791.json
|
||||
│ │ │ ├── 1db075565e2fe1027675b7ee03d055150e6b741f3e9865bcd46d9a6113c538a8.json
|
||||
│ │ │ ├── 1f440edc6feb5bf8c2a462b1b4a0b44bd22b4a763286fb2b340c797c282b5bcd.json
|
||||
│ │ │ ├── 1f6cfd55d13bc48aa6c68558090ceeaf24a54c7bec690d51d95243c904daf0a6.json
|
||||
│ │ │ ├── 1fabd63a56e826ff351d3edca222f5f958004c01f0bd16aed30080e8b13da33f.json
|
||||
│ │ │ ├── 20107b827654d2af6301ac0a72a7d29ef6ef423ccde3e1fa48f5f92490fdfa90.json
|
||||
│ │ │ ├── 20a6a77982f0ffa677ecf8b96662481ca9acccb7ef05274a31fd7ddbd217842a.json
|
||||
│ │ │ ├── 211549439bc4b41e71f437e9909b8fc7bc306273615743fc61f53c8b8dc57e56.json
|
||||
│ │ │ ├── 21f38c2f6a379ab402b4d3bd1b0809129c8554ebb01202a160c42a1e9b49b717.json
|
||||
│ │ │ ├── 21f56aad8baf0dfcbc69c801ff4f738d0bcb84270150cde91f6290ebdef0e2d4.json
|
||||
│ │ │ ├── 22534d744aadf9c9b61b908eccea0e9b3eaa30bc381bace54b3221ee0861a0ec.json
|
||||
│ │ │ ├── 22df765bbeaca09caaf2fbcddcdd3ef1d4c0800f0c545f963bd91e85c33fe7ec.json
|
||||
│ │ │ ├── 2396802d9ca82a055f431939b4cd95eea6c29556956075d1d1ff35e4bca3b7c6.json
|
||||
│ │ │ ├── 244419b0eb45bd51409a2284de828aa5de674be78dd572f04c911b105a6c9ee7.json
|
||||
│ │ │ ├── 2447edb7f88778fbf1ff0ac017796eb52bb2ced4c91786eef12a341b37adfaf7.json
|
||||
│ │ │ ├── 25513735be357804874f44a0ab8d868419bbc8b0e33ee2bc20ce677554c7e43e.json
|
||||
│ │ │ ├── 2609e627752bd20df923f6c69e090173a368757b3dbd2ce418e3cc9f67c697ba.json
|
||||
│ │ │ ├── 2667c1ca679408fbaca6212c012d64f504eb8ddd574841569403de39c25dff3b.json
|
||||
│ │ │ ├── 26c6832fa65fe38fa62f98bbfd8d544cf6a470cce547579e1d650390e8b9fded.json
|
||||
@@ -264,8 +310,12 @@ customerTicket
|
||||
│ │ │ ├── 26f1fc81bffa8c22334482cd1cc638ac1f38647ae723ac8c360dd855281598e9.json
|
||||
│ │ │ ├── 287fdc570ea3598f1b62d5764970740f7397df117fd25e9340f03b0f8d0e5171.json
|
||||
│ │ │ ├── 28cefeff27c9cb5760aead796c37afdb1311bff0a5ff759136f0473ffa0152ff.json
|
||||
│ │ │ ├── 298888276da87757f59b0e0280ea46039c0ee05945961c3e00b287abcbc6a2f0.json
|
||||
│ │ │ ├── 2ac9a38700a628e196b14c23c8615d221fc2efdf0a9564722d4ddf4e293e70c2.json
|
||||
│ │ │ ├── 2b104842fa1eaead6f52dc015e74fa38c6327cfa1a95f0858e649d093b1e98bd.json
|
||||
│ │ │ ├── 2b7cc5d0dec7234082c1797ab637b916ac7df39277f8fbf109ae90190fc1e900.json
|
||||
│ │ │ ├── 2c8d91fbfca457646cf60edb03fd3a89a2494a6b6612087854edeac5a119739a.json
|
||||
│ │ │ ├── 2df1edd1fd32c467f237babe4a94f2ed73b67270ede6fa8298b32eab7c3eb80b.json
|
||||
│ │ │ ├── 2e17dd08443df447b74080559d8cfc6b782adfc04963abfb7473e8a59c21d701.json
|
||||
│ │ │ ├── 2fd5953e7926f320bdeac2d559967758aef4632bf7dfa26a5ee50ad386fc9b67.json
|
||||
│ │ │ ├── 3142f9d4517f137483c716dd61985811bbf006b85506791e9a2ee37d58b2f53e.json
|
||||
@@ -274,31 +324,47 @@ customerTicket
|
||||
│ │ │ ├── 32c40d17e90ccf2511e124b4fcc5b3674a7200dd5215ee8742d319f8090b04ba.json
|
||||
│ │ │ ├── 334f218e8e74fa3ac3e285059183c77efdbfe99ccfb426df17b4eceef14627e2.json
|
||||
│ │ │ ├── 3358bbaf4a10bb5102cd25fe7cdb5b8401880e8ec0d711b9b9e9b4c92404865e.json
|
||||
│ │ │ ├── 33fc3acd587f6970b19f708df61db16a5256a1914954829e2c622dacb8026331.json
|
||||
│ │ │ ├── 345cea9e55c5f1954119121edf5ec28f1514b11d83de6c5bddf80d014bd98c94.json
|
||||
│ │ │ ├── 34b48eae711a65d522f81081262d437469f08182368d46643dc80eadd96bbdb5.json
|
||||
│ │ │ ├── 36000938c9c3b13fbd92622f905f8c4f4bfbfdabe66e3357e88c162a4fb198ce.json
|
||||
│ │ │ ├── 360e95dd7f9428a51961a1f4ae7968a6e3101b8df00027432c6c743f64ccf9f8.json
|
||||
│ │ │ ├── 367d8bf55d8d85048571009c40b04ef43211cc9c630af6222fc5db65fb382cc8.json
|
||||
│ │ │ ├── 36c68425778aff8c4320f570cc4e4ec8b3c40094abdeffc556f5488085c587e5.json
|
||||
│ │ │ ├── 37159060b34fd31f9b937b215a65baef4b2091c310ea6430960e1d4991a37c5c.json
|
||||
│ │ │ ├── 3948717314f47afa835e760073df4764ac427f4858f5ec0ee6a2f298ac393d49.json
|
||||
│ │ │ ├── 3b878a979cc2ea2ac926f8a72f24dbef940bbaafdfbdc76679f4054a3ebf791a.json
|
||||
│ │ │ ├── 3b9252049dbf8ef67e64a756bb41325a6cf50190004d4442119d438620bacc72.json
|
||||
│ │ │ ├── 3c4a42b0b79b95acf81967479efdc4cd0a369e3b53b771fe87478d6327ddf0eb.json
|
||||
│ │ │ ├── 3cec8055583c7d94e03ced535376a8bd9d3d335cd6c7e328f38dc2a9e6b1e397.json
|
||||
│ │ │ ├── 3cf352346fa2dbd498d619b0d994f6c5b1f5a86f2e61974e24d85a836cdea0b9.json
|
||||
│ │ │ ├── 3f0cad4a4a6eb440a2b133bf72f2e72d6e7ef3668f4c8dbc2c759dbd1a41eb6f.json
|
||||
│ │ │ ├── 408f6eb568e45e950fdc170776dabf9087829df0db630e16643fba8995318d26.json
|
||||
│ │ │ ├── 4177b5c9bcdd159b716f3e956852b337fc54e7e597b5631d0f0e9a4039011901.json
|
||||
│ │ │ ├── 42ffbc76dd6afcd90fde30281453fcc765ed07bc0e729fae4eee28d86f840578.json
|
||||
│ │ │ ├── 433946952194be24247d30e4d5ba4d67b64ee2cda27a9212f16b4160a6f3fb71.json
|
||||
│ │ │ ├── 434bb9df51f8929ca10167ce360dbaa9d93fdbfbd27053bdf11f3b171189edb8.json
|
||||
│ │ │ ├── 44684e85104b3a520e0369a953041469689cb680c2144a6c3fd7107cadc9eed0.json
|
||||
│ │ │ ├── 454699d97755028c24e254cc79a0b879de262c5931178ee5dc201f64c3e13f39.json
|
||||
│ │ │ ├── 47730ea39698b58e7a30a7e5fce471272e9844e0a0c09a0d603699446667a9e2.json
|
||||
│ │ │ ├── 478a054f8292cd197743a61b5e3e04487f67f8db76ae658a28b04e4aef670ab2.json
|
||||
│ │ │ ├── 479992e96c0c5fac0a00bd4d9e0822dd52273158f5364c26884649b017e5b874.json
|
||||
│ │ │ ├── 47fb67d01a9006c7f835ad87b75b95bb1365c27b342144f081d858e07fd309b3.json
|
||||
│ │ │ ├── 48ffc385500573148218512753c10924e3d70b9b1cb255782425e40063572449.json
|
||||
│ │ │ ├── 4993b53d428cbbb1e334ea396aae6397586a1295a35f39da038cfe72f0b89fd4.json
|
||||
│ │ │ ├── 49be8523d744110ec6038ec61ccc9e8f47894a392b60e970206c512c27e68178.json
|
||||
│ │ │ ├── 4ae084687e4372e69f349e8bbc5cf603768a032d3d9ee4d2504e6709ed8ee440.json
|
||||
│ │ │ ├── 4ae419f04a3a3b1221faef43b8526910f1958336162fbce432bab0e596ef2aa1.json
|
||||
│ │ │ ├── 4bcdbebd8ab168095ad8130c68a697dd5ed64c3f52350fbd395c37408b9a7afa.json
|
||||
│ │ │ ├── 4bf46c6c0f27e14312f7c1311bca1bf5e1bfee29ea5cb35e6f94142141985d23.json
|
||||
│ │ │ ├── 4ca6d659e3dbcd21e9bfcdc394e19ad8b4dfb6a26665f3395e5a0d5f41e611ce.json
|
||||
│ │ │ ├── 4cf64fe0b79aec18c49355f7d82ab04ae66067042a6c364e89842109427ffb5d.json
|
||||
│ │ │ ├── 4dae69d87cc12f2afcaf76a8124cbb0ed4ed5b1b196563ead3179384bf5cb5a8.json
|
||||
│ │ │ ├── 4ece1fa07b9b0b7d50f5efe48a083265e3d8efac191ed4a14875c8399522db42.json
|
||||
│ │ │ ├── 4ed0e6e63784e85d5e256cb6eda996383dad486161209fda91200caa511e8907.json
|
||||
│ │ │ ├── 4f4799a971a5d6701087428e8c709396c0d947f7ce5bcf7a048cd542d5551c57.json
|
||||
│ │ │ ├── 4fd59b7f74524c4eb0b9293df963b8aa007039c53e88fef4ca2bd9b61197a114.json
|
||||
│ │ │ ├── 508a0054ec5c5b3353e94566a0f728d533ec47721e20e6037aa55e2d665a5143.json
|
||||
│ │ │ ├── 52cd74ad5803b940489587043429b741434ae931fa4d244f5b0dc53582cee050.json
|
||||
│ │ │ ├── 53705e64605d8b06954f91829d03ceef80bfacf943427a2d51079c554e41a42b.json
|
||||
│ │ │ ├── 552bcabe2c76a5db8c1f071dbd2b19c31aeee0b07cf605cd5f2b031ea3b6440f.json
|
||||
@@ -306,23 +372,33 @@ customerTicket
|
||||
│ │ │ ├── 576178824e99e86dee1ca5fcdf46f61962380f6b8a708b046fbdfc766eb650ce.json
|
||||
│ │ │ ├── 5794ed0ca9011299d935a3be468c921c65ab616e78f0c6509ec22e3a77cdb794.json
|
||||
│ │ │ ├── 57bc96866536f0d67300ed29e56bd0f45fa9119c787fac805f228adcab18dc6f.json
|
||||
│ │ │ ├── 582c91a8ddabbea5521fc65d724e4fb3672c070f9213fae7b58595fcf62f3c85.json
|
||||
│ │ │ ├── 596a13e9b322f8c3332a3ae214ca7bf368ba6158ff3adcc7db4582f102ea51dc.json
|
||||
│ │ │ ├── 5a34ded8b2dd4c10c4251d287580918b88690c7058dd2299d2d670170104241d.json
|
||||
│ │ │ ├── 5b939ad6be2b3eb4da79f453386a58c6ff6661e02d3089092784a774c1957ae4.json
|
||||
│ │ │ ├── 5bb0895d417e210a10ebf2c63ecf3185dca651057d952203e2e39abc249180a8.json
|
||||
│ │ │ ├── 5d43f2c9afc3451745d2a1b5f1ed17bc138c9b19caff61beadbc40a4e970b525.json
|
||||
│ │ │ ├── 5dd23426751ac130946faa8dff76b2200a8a6c5515863890c15ac5c6c3a542d3.json
|
||||
│ │ │ ├── 5f1bede7e08769186ddcf9e524e41c839ca5d4ead29bff053524cdfa59654e2a.json
|
||||
│ │ │ ├── 60ef2d6fded9e070e35cfed119e8325010aa76bce63aa74ca0f25005d8ed9517.json
|
||||
│ │ │ ├── 62dab578f0debe93a8df61a1776953980abd254ae6f1eea77afb00dfefd28f15.json
|
||||
│ │ │ ├── 62e55882d2ce81dcfe5df1d58395e767ded55d6e39997d82cc3489c5e02ae910.json
|
||||
│ │ │ ├── 62fba7c993ca0a10198cab8e5a7538f39f88c00face1d324cbf4613effb5ee1b.json
|
||||
│ │ │ ├── 6469cbb1dbfa0b98762fde08c6fd05cc831e1cf21bd61aebb56b34a7e916bb1a.json
|
||||
│ │ │ ├── 6506009802a6a268be82b680ace7580613f18dc702ed27aaff929398a85760f3.json
|
||||
│ │ │ ├── 65c9b0921f2ad9b0228f5685c40d5b341da9604f8cec59339fded4cf0bfa40f4.json
|
||||
│ │ │ ├── 6802a4e7d920a2a5ce48a61989565f6fa769c314a01b9179cb9d44653b9467ae.json
|
||||
│ │ │ ├── 682101a547823a97feec07270aac7a9176109731e376f49d3429fe01db3e8719.json
|
||||
│ │ │ ├── 688d055d3333b15b1beed13c74d14b507a1db1400918f22f01c3bc37bdb4c84e.json
|
||||
│ │ │ ├── 68f74aa5f41fe3d0e77d36da903e26c49346ee9226c8218a71f7b44c2c41bd93.json
|
||||
│ │ │ ├── 696bb846cc6a3735e085b502a1008e270903c81c7a7adcc7676abcf1930e926a.json
|
||||
│ │ │ ├── 699dc95b512c50339855225983dce69519cdcfd21b830ef92b94c885ec7b9f6d.json
|
||||
│ │ │ ├── 6a7b027b7f77736aea87ce048be3247c43cf4878e8a1105811247839427ebcf8.json
|
||||
│ │ │ ├── 6b60f8a4e1c97f193edc097890ea2759ec246b8ea09035b2afe8b0b32181f345.json
|
||||
│ │ │ ├── 6bd49215f8d2739a9e365dcc8855b95cfa34352c067a9e9ff48a623318c4ddaa.json
|
||||
│ │ │ ├── 6c69494ba124d4bc564bcd3a3df1f33ae0ee0769001a2dfc46825ef8b44dc3e2.json
|
||||
│ │ │ ├── 6c983917b91998d7130b467295146438ee4a631519fb7c54b582340f90bfc7e6.json
|
||||
│ │ │ ├── 6d3b094dc56f9b26ce7374ba28e460b85248711223cf516f8e732c5d8f4d9d75.json
|
||||
│ │ │ ├── 6e56bc8cfd4ce0c21b56fe2b18b7923c24b34cc09ff39276d1438666c54b074f.json
|
||||
│ │ │ ├── 6ee9154979fb497a8ec229a7177660af53a487e1aa17c7b555cdc67797615162.json
|
||||
│ │ │ ├── 7093cbd0a855750457814388b6e527fd3b612163f7a2b898ed6ce470dae74843.json
|
||||
@@ -338,14 +414,25 @@ customerTicket
|
||||
│ │ │ ├── 7805df77950548e276a1dac439cf36a3620025bdb8a5739e7a84237f38d01a8c.json
|
||||
│ │ │ ├── 780c2256439479b3eadb498309a4309b20313a36633aa8bec2a0dfcfce3a3d32.json
|
||||
│ │ │ ├── 78381b49b02bd2a6248eb0363cbceaafe887dbf8e6b19dc513739b35b87cca95.json
|
||||
│ │ │ ├── 791d193245824d3da6413fd877ab4a079771d29f800f41c9c4774c86f1a903df.json
|
||||
│ │ │ ├── 79cb5047a177592ad737ee438a5e0de370e5dcd28b164b5a48234a6e4f554b1e.json
|
||||
│ │ │ ├── 79ede3c31ce20a16af1a767ed248e4a7840803b6d34cf90f1205aa36f3acd7be.json
|
||||
│ │ │ ├── 7a886aec3fe4d432060a7ad6c88a9e0fd810a122ad1d244456af089a828af8c1.json
|
||||
│ │ │ ├── 7aff3a4d59fae95d952044a679293bff08c7305212ff12bd7be1d314e1f25a3a.json
|
||||
│ │ │ ├── 7b25ccdfeab6d57509c1b8a9745fb601ab7e8fcae0e73a22932ceb725fd38c9a.json
|
||||
│ │ │ ├── 7b6916f499ae0ceae3eb3d509b51f42a7eaa98e43e161fe87c44b9a5bb01287b.json
|
||||
│ │ │ ├── 7c9789f4b51a967448da1de52e4ac3bfb4b342284bab1257343262aaa32b0d6d.json
|
||||
│ │ │ ├── 7cd278b7ea82af8777e4da2f84fd1773dae3dd371744f66bbe12c0b925fd6486.json
|
||||
│ │ │ ├── 7d276789b80c8d9d1677556b3fb33b2da05988e97f6bf28903a044bb9fd64894.json
|
||||
│ │ │ ├── 7e136779a95d8e8e2ac6cad2192890254fd0d54d46b4f5514debba0f22618d75.json
|
||||
│ │ │ ├── 7e1c0a38a3ed90205d1829bdef528887ef12c928d4da797e06a79b3e7a1a92ba.json
|
||||
│ │ │ ├── 80468d8a995fb4092dfe5e57e25592c06713287d48367b675ae4d00955d4b25f.json
|
||||
│ │ │ ├── 806a3b07b690ca3871b1854aaff6f9042e48cf7aaa9bb9b6f15ab9634eae3b37.json
|
||||
│ │ │ ├── 80ee0e1cf4fb023c632197378ec4601f72c438df11001fe2648f59ae2aa89199.json
|
||||
│ │ │ ├── 80f142842848c4420eeec86959e77c24566467dbece6a275dd9c6da15f98551c.json
|
||||
│ │ │ ├── 8206f93340496777fb5c0c2b431493c2d243c12fb74dc8cac46123d15f9d3f44.json
|
||||
│ │ │ ├── 82fcc152dd67510406aa894d1e38939bac365093de9f34db4dfe2485806fda4f.json
|
||||
│ │ │ ├── 833fb89a8d72afc195b8f5bd7338109d8ce7be79f240dac31c4dcd36c8c674f6.json
|
||||
│ │ │ ├── 835c014cd4e3398cae6e9466790acda90cd37183b9a777ddece3881351026c3c.json
|
||||
│ │ │ ├── 84579b3014e1a83e4201862df1e93378f5f953fbb547fea30965e335be5e7b1e.json
|
||||
│ │ │ ├── 84dedd4165eed360c4051690b2aa84f6ecc08f3f053f4129ca6f3e33c334ba35.json
|
||||
@@ -354,21 +441,29 @@ customerTicket
|
||||
│ │ │ ├── 88ade181c6611ec33d9bcf5fe739dc809d81875b6571b9f60f87dafb4f5941b2.json
|
||||
│ │ │ ├── 897df8bbfa3d5a2c272916d47f3326de2b1cad7c3046015c949a46f69e9b8247.json
|
||||
│ │ │ ├── 899b16856382d32c4e5b39d02cb251bdedaf35c5a81bede5c19bfaff092208e9.json
|
||||
│ │ │ ├── 8a33ace67f0a10b86cbecbba57107ea42ee61fd12b496085dc98f23d8b300c99.json
|
||||
│ │ │ ├── 8aeea7acc875fd642cb75f8766dc1fa84e8f91f027c3ced3bf44b8faa1a2798f.json
|
||||
│ │ │ ├── 8b45a75ab824f6e8b21a94f6828c0da172fd242fadeb1f61b32de34b80f62cee.json
|
||||
│ │ │ ├── 8b7bbf32006c5ceccb03e4965f57199e55c6309eddcd09e544d7e64e8e383cae.json
|
||||
│ │ │ ├── 8c258f28e7a23d569a250b17e2f591ea2b1a5b9072ab1e0ddff1b535468badb7.json
|
||||
│ │ │ ├── 8e216def3e5fce2d7f4db160b9bc86433bdb5f0638be1367af2ab095d06571b6.json
|
||||
│ │ │ ├── 8fd86558bd11e7a00f6b28cfad545707ef94afd87a9095ab532588b36c49f842.json
|
||||
│ │ │ ├── 8fe2c3236c2b05a4814f32898daea17b341332cd0cc887df7a154ad999480d75.json
|
||||
│ │ │ ├── 9141ebe765f15cd7e3b24431f315af03e41a165ff0a54c33bec2c4884515eff2.json
|
||||
│ │ │ ├── 91d83c62f39f219ccb6125502bf8e0a71a587dfc0a66d977a9bb93c8f2dc5dc6.json
|
||||
│ │ │ ├── 9326d87e36b2dc24e6173aa3346f46e7df9fc3353fdb409304af4363c286a41e.json
|
||||
│ │ │ ├── 939d30ca6a3e0c8379078425d21de30c3e8c2743ddf3e13efd984becbf68d109.json
|
||||
│ │ │ ├── 9697c37cf3a88d2b4eb5419b2381951a9aca966172316db79e7b4c32557d5e87.json
|
||||
│ │ │ ├── 972650f72e30ee3a72cd37dedff2f0ba823b355300dd68c5e196f87717bd5bbe.json
|
||||
│ │ │ ├── 9729a8690e1957a356322dc991c0618b49605bd58709dcf8ce3ab1af7d254a1c.json
|
||||
│ │ │ ├── 97506546de142970349e57e8a14064280e66a4fcdbced5dac55fc263630a2247.json
|
||||
│ │ │ ├── 97d8ebb860cbcd63be9ef80c6e2094e4a7ccba668b291d3c4815133d5fb8f37e.json
|
||||
│ │ │ ├── 980f1be05d605b9093619724462c9881a8be063a5439567866172a35f8d22316.json
|
||||
│ │ │ ├── 9931a5b7d7ab58794d003cce1d0719c201c623a844c7f834f66c88e9390c824e.json
|
||||
│ │ │ ├── 996411678d23c3cf8a0e6c1c5076701e8bf5f3dad21ca907ec4116d15366a668.json
|
||||
│ │ │ ├── 99f94b7b3b60f627c482cc195709b98cd2faa55f14c19c4bf5e0af8a88f34b7a.json
|
||||
│ │ │ ├── 9a6a5b3e4a7ac5efbe662c2a44a7d9400c2c46f18f15c5dd3627deab4bd7c8d7.json
|
||||
│ │ │ ├── 9b267b0c45b93cecc1294ca1e040e5a18fdc9e0bee69a0ca8571ee11becdee97.json
|
||||
│ │ │ ├── 9b8bd2aff50e08b6f053b74a409c9439db41e692e29aabca5b67057884d945af.json
|
||||
│ │ │ ├── 9bc00a73e1e7393f535b9f58edc50d42169c407c58af664ffdf838c533bd29cf.json
|
||||
│ │ │ ├── 9ce16f69c6b04f703adb24ec7498a2d2ecbf6f7524c459deb25111247e550698.json
|
||||
@@ -376,20 +471,31 @@ customerTicket
|
||||
│ │ │ ├── 9e5144f3933fd4424f1e271f60b78ae110855d238a7921b499b1586d21ad16a9.json
|
||||
│ │ │ ├── 9eb524ba1a6374b2d29429652e7e8bf087d839fecd65494380982eab349382a8.json
|
||||
│ │ │ ├── 9ed5e291859abba38b07725bc6cf0275a1548e937888a5ba177674afaa48b96f.json
|
||||
│ │ │ ├── 9edcaa74bc64450dff4e25af0b02a830bf940c608b2cfaf74092a2d830f8b07d.json
|
||||
│ │ │ ├── 9ef0e904ba6d1b295c75a1d5741851f571339a8e22c7de96bf5e2508f764ee43.json
|
||||
│ │ │ ├── a04a8ed5ca0ac9661dda4f6f369248937cf67e61cc982a1f17b24495263d53d6.json
|
||||
│ │ │ ├── a05b3a5bb6e91642075fb765d43a0a6e4dc595bb9131be86643e6d06e6fc3cd9.json
|
||||
│ │ │ ├── a063cce1553aca090b61f3b2b621a089d1c128ff347e11f836857b77ef5f3d56.json
|
||||
│ │ │ ├── a081526ffd8f52690e0834cc71e9d2bf987f949c2d0de68a6eb024063d97e64b.json
|
||||
│ │ │ ├── a1bd5a32741129c967a18413ae1be6bf3e20754251066d342d652a668ef95025.json
|
||||
│ │ │ ├── a1d213c52d246524be10c084f9134e77ff2e2a9aa4361b71dc4bedc89f461cdf.json
|
||||
│ │ │ ├── a3c770875ffabc18c595532501255df82b1a09246ee54b831e7c2221932d4f88.json
|
||||
│ │ │ ├── a3f339fe1ccb99a051c1a3bff70e7de0e2ee6b952db0aa2d6739e97331c29cd9.json
|
||||
│ │ │ ├── a4a27e7b3842713c361895d6d9c5fc9e4341cce71c5842f014748e1c1c23a197.json
|
||||
│ │ │ ├── a5231d68fe35e6b02ff4ff70e02a3229c08d9b6c2bae774a1b3227823b95e15a.json
|
||||
│ │ │ ├── a5c3aedceeab625bb2f60c9e97163491579353e685d8b230342a4e3e30f00db6.json
|
||||
│ │ │ ├── a7729f0b6bf20ded62ff7b6ee678fdaae9516dcd80c2a8f9e9f3d9299f6e22a0.json
|
||||
│ │ │ ├── a7b036e5bd51876694ebd883bd1b0a527c5f8409b48d4716612311e954a8cc0f.json
|
||||
│ │ │ ├── a8274003d8d4ef62b23c66e3baecf14eea260f99a4509411d58506f560c2b474.json
|
||||
│ │ │ ├── a90d163d984bed9136db40b58b442e5ffa6561b2b6cca5ea6dc45fcb70b11299.json
|
||||
│ │ │ ├── a93b84a0def4d2e0e97ff35032871b0ce64786ff468912aed21fd958b9fe782b.json
|
||||
│ │ │ ├── aa56d5609649d9327e5f4cf6d02b5e8afb7749388f6d4f8776dcf910e5453935.json
|
||||
│ │ │ ├── ab1648cc3b4d4bba5754e19eeb80ea160fd20207d5530ac371647168f9ed211a.json
|
||||
│ │ │ ├── ac454f6bfed5913148eb8c9caaa0e30f99bac381936b44612bdee8274bb27b29.json
|
||||
│ │ │ ├── ad1175b041ab42a2615856a41fbab394ae63ef9dd991901c0658c3fb0846ad3f.json
|
||||
│ │ │ ├── aeb59c5e1447023f9c49003f6bf4eb26a7b9a34ca5658f7ba2cde1b99e25ec76.json
|
||||
│ │ │ ├── aee78d4e87210ef46180d47e7015345bd66a7763cd140d84cd86539e720e8e67.json
|
||||
│ │ │ ├── af49d8d606d42a439479739b7b89b8b13fd99adc7ef7c0dbfc456229e6690e6d.json
|
||||
│ │ │ ├── aff120c654cfc097d07e9078775b97e4f066e7c539b21b49397a372086d45543.json
|
||||
│ │ │ ├── b390f7bae9f80c48e703a6664d9de6ec9c141cdf8ac2f012d67ac2b4d8a25b4e.json
|
||||
│ │ │ ├── b39d104e50bfbd13e83fdf946668708536d98cd0c6228eded5fabd5bcf6ddd18.json
|
||||
@@ -409,57 +515,85 @@ customerTicket
|
||||
│ │ │ ├── be6cc64f9c2a8edbdbbd10a1a31e3b3a746eb4bddf806c71d525eff5719b9217.json
|
||||
│ │ │ ├── beb1b14e942397ae4b9f2e8a743282f127cc9f8c6e7222db7a28185de4493e57.json
|
||||
│ │ │ ├── c1060dca2a3f437f386324975be659215fe9f7e689137dc85e823879b462d11f.json
|
||||
│ │ │ ├── c10793a36312ddf2bd8ed9edbae5d67a19f54b78ac3cae01682d38c34f2bdf11.json
|
||||
│ │ │ ├── c1943dfe9eee1a9f88ea571dc5aa06a3f521241fffa51d55b1414c675e63c236.json
|
||||
│ │ │ ├── c199fcf03e3df0de15c0e7fa5f89fbd9649f2be4a257c8de9a7ebc69ce0d629a.json
|
||||
│ │ │ ├── c1a36d35264d786572e00bd20a4d62c18000e4a5a744b4bbd585c66ba2b1afa4.json
|
||||
│ │ │ ├── c251d835538bf3c08a96268c8b1feaae8084a9c298d634ce2f2ee3b4b15188ce.json
|
||||
│ │ │ ├── c2b1cd2a2842bf81134b918097c420440c56e69a46b7796fa07d02b2db7a3fab.json
|
||||
│ │ │ ├── c4878d8d8ae19e705821ae40afc1ede4be1a64b559dfdf89f898cb4383718c93.json
|
||||
│ │ │ ├── c8c8647cd740fbc96327657baebd4b4a16bfa1d8e7d4cfe3f845a0a5ab7c98f7.json
|
||||
│ │ │ ├── ca42945436e914d9530e20dc780794451ec6ab51e17414e8c9107ccad3125fd5.json
|
||||
│ │ │ ├── ca8e9d2e76f32d42a0da88c9477956b47dca6cdb27504d8d126e0075306eae98.json
|
||||
│ │ │ ├── caac8d56f4ba7b3987d86c31357c6ffd2c4125d16e4637cb5a4568ac6eee29de.json
|
||||
│ │ │ ├── cb744c0a1cb5fa65e239993bee622ad4a60990c628cd1d4503f9e8806d1d2c92.json
|
||||
│ │ │ ├── cb87cd70f59a8e0950529ae42a2229a259ff8b305e4319422fb1a9dbc41e63e2.json
|
||||
│ │ │ ├── cc5e32b33a50eb2225359c15bceebc406d04a07218cda65de3e6f79603d67416.json
|
||||
│ │ │ ├── ccd15ad60b63cd29e2bd2762133a74e2047a2894fd67206dff7f7c7fd082b399.json
|
||||
│ │ │ ├── cd4804092002a20d5ed88c32b577b0868a0ef39e518c749dbadec455775917fb.json
|
||||
│ │ │ ├── cdeaabefa47597c8be9310653d52fb6870d859162b49c8dae6db8f4f699dc4cc.json
|
||||
│ │ │ ├── ce52c1ffbf8db0d0b4ba85e0a07055258a8eb179db097f1df874e5be81aae49c.json
|
||||
│ │ │ ├── cf1725a1db0d7ecffa63d8b1b693854170e55385eaa4b0cc604f9d6ab204eb4e.json
|
||||
│ │ │ ├── d0765ede907ec0d299b023be2178bd7c85b9d37463b94c29454a904069676cc1.json
|
||||
│ │ │ ├── d083dc75811629ffa77ea30c1965e67ef3bc2bf65e5b20a1a35cb0f80a46949c.json
|
||||
│ │ │ ├── d210ed7569b9e06acf7037d9a7d15ccac0f6f95ef7d3a0707741db8629443177.json
|
||||
│ │ │ ├── d22b1453361617f8ed7de22e5aace7a02cfe67049aad5fa4b1849d9f8b42b73e.json
|
||||
│ │ │ ├── d2513d4f5ff7e6088864ff497613f47d4083c1ca378649a3f1db6d45a3f3fb2b.json
|
||||
│ │ │ ├── d26a8fc663e9e5fcf33c87592f8c0456e7fe9ec3390bcb902631144d455c2922.json
|
||||
│ │ │ ├── d48290356397f2b79e05fc0b6bc6f9c327da366096ff14c15e90516fad7218a1.json
|
||||
│ │ │ ├── d55e8987ca5ff35748b62290dc14150b1df53163c6f5a5cbe19d03058368f1c6.json
|
||||
│ │ │ ├── d561ececcdd4e7ff59595389fa499591f4e7f3c5324a58dbdd749ccf29e90cd1.json
|
||||
│ │ │ ├── d5dec4474de6c46020e987f20aa3fe54c9bb9c6726a067a75ed94091d552b87f.json
|
||||
│ │ │ ├── d8cda86cbe3f42bb98bb0097071d95924950dfc6ed65a7075d3b22ff1ae0bf0b.json
|
||||
│ │ │ ├── d9516c3232e0f78225764bf4f9586c88932df925c45de71f4eb1c883a9ed5b5d.json
|
||||
│ │ │ ├── d9c81db9cf9f2fee8a4985367d33b1c0ab1d7e6901e85473070c3bfd6bfe32c6.json
|
||||
│ │ │ ├── daa187dac6877c746c02269141a7e7db178ac32a0b3a4bfeaaa8ed046b6dcffb.json
|
||||
│ │ │ ├── db04dbc0e2dca073d2b145db8ba6c2c5299d3fd93742e1022c611c938fcb4d80.json
|
||||
│ │ │ ├── dc90ac5e8d6766f8001bc2a40d21c203717d7a9f469956c2f6d9bedf3ae51aea.json
|
||||
│ │ │ ├── dd795078450fe69f61a445d3cf1275420be57381b005597cb5003859b2025800.json
|
||||
│ │ │ ├── df2d803fc69084e91df819bda27a7e9f7303a40ff7a5d30d93fbc16daf5e2d45.json
|
||||
│ │ │ ├── dfc9279b0d9023afc1dbe55536fc1cd7a556a6a437775128b9e41615431b4733.json
|
||||
│ │ │ ├── e0373f56a7e20838a75d2c1920393433e8d9a6ffa919334851b70f210ed4dbf3.json
|
||||
│ │ │ ├── e15f9a7e0698366b7d8b4bee6102d351241860a078550f47a1eb27503e521bbc.json
|
||||
│ │ │ ├── e2c372f92146217844d34664334cb0d7f01fe262ff98edf009a66c4bf4743b5c.json
|
||||
│ │ │ ├── e2f4f5c0bcb971ba3cb08a4ba14ed5d1ba6b5899fd36db1f5dcdbd2313cd318e.json
|
||||
│ │ │ ├── e359eb0e19f76b69decc0ec747a7fb4c9843046bda31c5e45841858e01bcb5a7.json
|
||||
│ │ │ ├── e42f6a96ad8938a6ab248b342feca7ec6ecf2a734d25141f62dbc9a6c52ecf6b.json
|
||||
│ │ │ ├── e43b7eb1fd2770d280dedb21e9a453279f3aa8262b2a0409fffe8fa9f7016880.json
|
||||
│ │ │ ├── e53a0dfeecd1b547e38896f8e7920e6801d84dbf61bb4513541892d44d42ae99.json
|
||||
│ │ │ ├── e6b4123e64b20d577378853fe71efb3743dbe9aff24ca06067b4a2c1c2efc53c.json
|
||||
│ │ │ ├── e7c0ad70ad7e7f3a1f6f35e55f4cf87f6c0850abc63de71a418e81b88ec826b7.json
|
||||
│ │ │ ├── e831be2a985dcf2094a014d182ea02e4ee10b94fbd35b44e3c69a98a7acb3451.json
|
||||
│ │ │ ├── e90aa709aa08fcf3294352b588ce8c8c902f3ef59a503d4b8d9a292a9c478096.json
|
||||
│ │ │ ├── e974cd0006d8160a6ef1316083bac9e699adfd87acc7edb404ae7eba1c799b68.json
|
||||
│ │ │ ├── ea42f92a2b1964458b3c1060b13a607581b30e9b43d5e031f8a263cae90ba581.json
|
||||
│ │ │ ├── ea56445d1314587e18b1992d9599da03e1419ea7bc4929854b9039d6947de33b.json
|
||||
│ │ │ ├── ea9a2571f9019bc6436c81714fa79bf8321273fc60de3ce5346893ee2f3dead3.json
|
||||
│ │ │ ├── eb1dd65efa5a99811ccca80ff03a73cb155ad86d13635010a44c09c13953604d.json
|
||||
│ │ │ ├── eb782e7a5b5695997a5bf628a22f4d36f5ddb886e5bdf4b2f4ae53ecc46fbca1.json
|
||||
│ │ │ ├── ec7939ed122c473ca1a4eee50f1c564e04e95ddced18cca56ed83efe5d4071dd.json
|
||||
│ │ │ ├── edfca154606b071ac823ab03572797546c6044eda88073e9e3f55b4960cb3400.json
|
||||
│ │ │ ├── ee19f3245c11f658fb60d32050a8bc4864ea289071ff5f829717ecacbac1d3e4.json
|
||||
│ │ │ ├── ee89093aa638ec9d38b76ba7adb66c8424d941d79deba2913d3d7085c1a062f7.json
|
||||
│ │ │ ├── f06f026d4fb736e9628a73bfaa9ccfe00efc006be79cb1c27f4a77ede5874f52.json
|
||||
│ │ │ ├── f12416bacd2eea21842fe89fcfc2c02ffe84619d7ce380d13679b64c6ec7496d.json
|
||||
│ │ │ ├── f150f764dc46f341076a8b373709a9cfefe48476672882b2a979df4624aa2626.json
|
||||
│ │ │ ├── f2069429b170b19df531b051a2f5fa81854a7bedf1a036aab1a46c89a9ba4c92.json
|
||||
│ │ │ ├── f2287c55ccdc0e5e9ab12f79895b1a1f46b65b2d14f70915ceb09adfc624e3b8.json
|
||||
│ │ │ ├── f411c6e74a1dc71e5f7a16f159d065410a8562fd545dd209179b54ae2e848e10.json
|
||||
│ │ │ ├── f4436ad45c3f2a27b4461333ae2ed21af157a3945e152fca7f43d640dc39f63b.json
|
||||
│ │ │ ├── f576af4a0cecb6cbaaf8afe5097913be0b7071e200fc924c918ea93446f00c91.json
|
||||
│ │ │ ├── f58927b2be96cb8769bb26a2ede3d8c05796e7a79fefd7fdcdc89d658609fc66.json
|
||||
│ │ │ ├── f71ec68c7b2a4467358d618354305f3f4e99cc0dbe60fa00d39478b686fe8bb2.json
|
||||
│ │ │ ├── f8769e3eeb23aebc01d72869a2bc6e635d7dc82440bc2b0b0e11e6b6b17f5109.json
|
||||
│ │ │ ├── f8fc55a028176d1ea9a1b580a8653d4c45ae7466445f37e6c23867f12821c44c.json
|
||||
│ │ │ ├── f92455d5b493a0d7e9565d2c7463a1aa3fe62fa20ea19e635e66c35582ab685e.json
|
||||
│ │ │ ├── fa19a1bc8027144f14106a6f90d4ddab0277580b8bcdb6e9e3ba67496c043d62.json
|
||||
│ │ │ ├── fb9736a4ef3aca74573f131c7cbd9f015d88dd755ffba3edd324aec95149175f.json
|
||||
│ │ │ ├── fc313334fba5b89ae7b30c44e1b0bd43e185bfd06c1e9804ad1b9b6de65efad8.json
|
||||
│ │ │ ├── fde577f1231f9b033560bfe075896efc6bf8d6079a030a159035584c7d39843b.json
|
||||
│ │ │ ├── fe032050843e4e1c28f5712205df74fa1698390169658c4ddc4555b6bcce8872.json
|
||||
│ │ │ ├── fe0c3eed2ff5c4b5d2116aba4494d599ae3c27004136e99f5d455f351281fa78.json
|
||||
│ │ │ ├── fe77fbd662d91d04f2efc69788d30e9859584c8d87d1fabd60ca66661568091e.json
|
||||
│ │ │ ├── fe93d0fb278c013183276f7bf5c5e2d0e62fe9c75f6695390f5994ec0b9c8dec.json
|
||||
│ │ │ └── ff1bf9257c3d267b5a04560ed07ac6d4442a145b3e6db5a0f8e63e8e7f386533.json
|
||||
@@ -534,7 +668,26 @@ customerTicket
|
||||
│ │ ├── query_20260910_053611_613a8cc4_qa_role_organization_unit_permission_scope_workflo.md
|
||||
│ │ ├── query_20260910_060824_3faac489_in__tickets_new__place_unit_and_category_side_by_s.md
|
||||
│ │ ├── query_20260910_064145_f562e795_برای_واحدهای_فروش_و_مالی_حسابداری_دسته_بندی_های_مش.md
|
||||
│ │ └── query_20260910_065345_ff4c3175_جستجوی_دسته_بندی_های__tickets_new_search_را_حرفه_ا.md
|
||||
│ │ ├── query_20260910_065345_ff4c3175_جستجوی_دسته_بندی_های__tickets_new_search_را_حرفه_ا.md
|
||||
│ │ ├── query_20260910_075602_ae6c7fbd_production_deployment_with_coolify__external_postg.md
|
||||
│ │ ├── query_20260910_125054_05ced73b_create_a_production_seed_from_the_current_sqlite_d.md
|
||||
│ │ ├── query_20260910_134049_86119b32_تبدیل_from_python_3_11_slim_as_builder_به_from_pyt.md
|
||||
│ │ ├── query_20260910_140232_c05525c7_علت_خطای_postgresql_name_or_service_not_known_در_ا.md
|
||||
│ │ ├── query_20260911_083025_6392fd14_شبکه_external_با_نام_coolify_را_در_docker_compose.md
|
||||
│ │ ├── query_20260911_085318_37a0a65e_در_deployable_docker_compose_و_تصویر_تنظیمات_cooli.md
|
||||
│ │ ├── query_20260911_090701_a5c9c0ec_اصلاحات_routing_فایل_های_static_و_اجرای_یک_باره_se.md
|
||||
│ │ ├── query_20260911_093724_97fb1f0a_بازهم_مشکل_حل_نشد__لطفا_بررسی_دقیق_تری_انجام_بده_ا.md
|
||||
│ │ ├── query_20260911_095201_b5c43bde_ناموفق_بود.md
|
||||
│ │ ├── query_20260911_095829_124db60a_deployable_docker_compose_in_coolify.md
|
||||
│ │ ├── query_20260911_100145_f7600109_آیا_استفاده_از_labels_در_git_based_docker_compose.md
|
||||
│ │ ├── query_20260911_102142_fb30dba3_ارزیابی_پیشنهاد_unhealthy_و_reverse_proxy_کردن_dja.md
|
||||
│ │ ├── query_20260911_102916_548d68f4_چرا_nginx_در_coolify_برای_healthz_و_static_پاسخ_40.md
|
||||
│ │ ├── query_20260911_143307_b8e47560_صف_عمومی_هر_واحد__جداسازی_پیام_های_staff__redirect.md
|
||||
│ │ ├── query_20260912_052547_361db71f_در_پیامک_تیکت_های_ایجادشده_توسط_staff_و_ارسال_گروه.md
|
||||
│ │ ├── query_20260912_090237_9dcd72d5_چرا_مدیر_کل_در__staff_contacts__خطای_500_می_گیرد_و.md
|
||||
│ │ ├── query_20260913_163601_80a69700_بهبود_ui_ویرایش_عضویت_مخاطب_در_گروه_ها_و_افزودن_آپ.md
|
||||
│ │ ├── query_20260913_171403_c1a197d5_افزودن_chat_id_بله_و_سه_شماره_سرپرست_به_هر_واحد__ا.md
|
||||
│ │ └── query_20260913_171644_3abcf227_اگر_شناسه_گروه_بله_و_هر_سه_شماره_سرپرست_واحد_خالی.md
|
||||
│ ├── reflections
|
||||
│ │ └── LESSONS.md
|
||||
│ ├── .graphify_analysis.json
|
||||
@@ -553,9 +706,14 @@ customerTicket
|
||||
│ ├── graph.json
|
||||
│ ├── GRAPH_REPORT.md
|
||||
│ └── manifest.json
|
||||
├── production-seed
|
||||
│ ├── data.json.gz
|
||||
│ ├── manifest.json
|
||||
│ └── media.tar.gz
|
||||
├── static
|
||||
│ ├── css
|
||||
│ │ ├── app.css
|
||||
│ │ ├── customer-ticket-wizard.css
|
||||
│ │ ├── dispatches.css
|
||||
│ │ ├── enhancements.css
|
||||
│ │ ├── operations.css
|
||||
@@ -575,7 +733,9 @@ customerTicket
|
||||
│ ├── js
|
||||
│ │ ├── attachment-uploader.js
|
||||
│ │ ├── chat-coach.js
|
||||
│ │ ├── contact-group-selector.js
|
||||
│ │ ├── conversation.js
|
||||
│ │ ├── customer-ticket-wizard.js
|
||||
│ │ ├── image-viewer.js
|
||||
│ │ ├── location-picker.js
|
||||
│ │ ├── pwa.js
|
||||
@@ -647,12 +807,18 @@ customerTicket
|
||||
│ │ ├── tests_e2e_test_customer_unit_categories_e2e.py__test_customer_switches_unit_and_sees_its_categories
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
│ │ ├── tests_e2e_test_organization_permissions_e2e.py__test_e2e_02_contact_group_picker_excludes_other_units
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
│ │ ├── tests_e2e_test_organization_permissions_e2e.py__test_e2e_05_06_limited_operation_owner_and_ceo_view_all[BULK-03]
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
│ │ ├── tests_e2e_test_organization_permissions_e2e.py__test_e2e_05_06_limited_operation_owner_and_ceo_view_all[INQUIRY-04]
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
│ │ ├── tests_e2e_test_organization_permissions_e2e.py__test_unit_change_renders_experts_when_a_legacy_cached_payload_has_no_queue
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
│ │ ├── tests_e2e_test_public_ticket_workflows_e2e.py__test_e2e_07_sales_general_queue_creates_verifiable_ticket
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
@@ -677,6 +843,18 @@ customerTicket
|
||||
│ │ ├── tests_e2e_test_public_ticket_workflows_e2e.py__test_e2e_12_sequential_unit_staff_category_and_attachment_state
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
│ │ ├── tests_e2e_test_public_ticket_workflows_e2e.py__test_e2e_customer_wizard_auto_stops_voice_and_submits_audio_only_ticket
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
│ │ ├── tests_e2e_test_public_ticket_workflows_e2e.py__test_e2e_customer_wizard_validation_preview_edit_and_floating_submit
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
│ │ ├── tests_e2e_test_public_ticket_workflows_e2e.py__test_e2e_experts_follow_configured_numeric_order
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
│ │ ├── tests_e2e_test_public_ticket_workflows_e2e.py__test_e2e_subject_stays_empty_for_manual_and_search_routing_until_user_types
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
│ │ ├── tests_e2e_test_pwa_versioning.py__test_pwa_registers_versioned_worker_and_matching_cache
|
||||
│ │ │ ├── failure.png
|
||||
│ │ │ └── trace.zip
|
||||
@@ -708,12 +886,14 @@ customerTicket
|
||||
│ ├── test_branding_operations.py
|
||||
│ ├── test_customer_category_catalog.py
|
||||
│ ├── test_customer_unit_categories.py
|
||||
│ ├── test_deployment.py
|
||||
│ ├── test_domain.py
|
||||
│ ├── test_env.py
|
||||
│ ├── test_jalali_templates.py
|
||||
│ ├── test_organizational_ticketing.py
|
||||
│ ├── test_permission_architecture.py
|
||||
│ ├── test_persian_text.py
|
||||
│ ├── test_production_seed.py
|
||||
│ ├── test_role_workflow_regression.py
|
||||
│ └── test_views.py
|
||||
├── .dockerignore
|
||||
@@ -725,8 +905,11 @@ customerTicket
|
||||
├── .npmrc
|
||||
├── AGENTS.md
|
||||
├── db.sqlite3
|
||||
├── db.sqlite3-shm
|
||||
├── db.sqlite3-wal
|
||||
├── docker-compose.yml
|
||||
├── Dockerfile
|
||||
├── Dockerfile.nginx
|
||||
├── manage.py
|
||||
├── package-lock.json
|
||||
├── package.json
|
||||
|
||||
@@ -23,10 +23,15 @@ def _magic_link(job, reason):
|
||||
token,link = TicketMagicLink.issue_with_record(job.ticket, reason=reason, created_by=job.staff_user or job.ticket.owner or job.ticket.created_by)
|
||||
return link, f"{settings.PUBLIC_BASE_URL}/t/{token}/"
|
||||
|
||||
def _sms_context(job,magic_link):
|
||||
profile=getattr(job.customer,"customer_profile",None)
|
||||
def _sms_context(job,magic_link=""):
|
||||
recipient = job.customer or job.ticket.customer
|
||||
profile=getattr(recipient,"customer_profile",None)
|
||||
unit=job.ticket.owning_unit
|
||||
return {"magic_link":magic_link,"first_name":getattr(profile,"first_name","") or "","last_name":getattr(profile,"last_name","") or "","recipient_name":" ".join(filter(None,[getattr(profile,"first_name","") or "",getattr(profile,"last_name","") or ""])),"unit_name":unit.name if unit else "","company_name":getattr(settings,"COMPANY_NAME","لوله گستر گلپایگان"),"ticket_number":job.ticket.ticket_number,"subject":job.ticket.subject,"province":job.ticket.province or getattr(profile,"province","") or "","city":job.ticket.city or getattr(profile,"city","") or "","expert_name":job.ticket.assigned_expert_name}
|
||||
customer_profile=getattr(job.ticket.customer,"customer_profile",None)
|
||||
first_name=getattr(profile,"first_name","") or ""
|
||||
last_name=getattr(profile,"last_name","") or ""
|
||||
recipient_name=" ".join(filter(None,[first_name,last_name])) or "ثبت نشده"
|
||||
return {"magic_link":magic_link,"first_name":first_name,"last_name":last_name,"recipient_name":recipient_name,"unit_name":unit.name if unit else "","company_name":getattr(settings,"COMPANY_NAME","لوله گستر گلپایگان"),"ticket_number":job.ticket.ticket_number,"subject":job.ticket.subject,"province":job.ticket.province or getattr(customer_profile,"province","") or "","city":job.ticket.city or getattr(customer_profile,"city","") or "","expert_name":job.ticket.assigned_expert_name,"customer_mobile":job.ticket.customer.local_mobile or "ثبت نشده","category_name":job.ticket.category.title}
|
||||
|
||||
def _render_sms(event_type,context,unit=None):
|
||||
from string import Formatter
|
||||
@@ -65,10 +70,7 @@ def _send_sms(job):
|
||||
recipient = job.payload.get("recipient_mobile", "")
|
||||
if not recipient:
|
||||
raise ValueError("Unit supervisor mobile is not configured")
|
||||
subject = " ".join(job.ticket.subject.split())[:80] or "بدون موضوع"
|
||||
category = " ".join(job.ticket.category.title.split())[:50]
|
||||
customer_mobile = job.ticket.customer.local_mobile or "ثبت نشده"
|
||||
message = f"تیکت جدید {job.ticket.ticket_number}\nتلفن: {customer_mobile}\nموضوع: {subject}\nدسته: {category}"
|
||||
message = _render_sms(job.event_type, _sms_context(job), job.ticket.owning_unit)
|
||||
return get_sms_provider().send_message(recipient, message), issued
|
||||
if job.event_type == "CUSTOMER_TICKET_CREATED":
|
||||
issued, url = _magic_link(job, "customer_created_ticket")
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
EVENT = "UNIT_SUPERVISOR_NEW_TICKET"
|
||||
TITLE = "اطلاع تیکت جدید به سرپرستان واحد"
|
||||
BODY = "تیکت جدید {ticket_number}\nتلفن: {customer_mobile}\nموضوع: {subject}\nدسته: {category_name}\nارجاع: {expert_name}"
|
||||
TAGS = [
|
||||
"magic_link", "first_name", "last_name", "recipient_name", "unit_name",
|
||||
"company_name", "ticket_number", "subject", "province", "city",
|
||||
"expert_name", "customer_mobile", "category_name",
|
||||
]
|
||||
|
||||
|
||||
def seed_supervisor_template(apps, schema_editor):
|
||||
sms_template = apps.get_model("notifications", "SMSTemplate")
|
||||
sms_template.objects.get_or_create(
|
||||
unit=None,
|
||||
event_type=EVENT,
|
||||
defaults={"title": TITLE, "body": BODY, "allowed_tags": TAGS, "is_active": True},
|
||||
)
|
||||
for template in sms_template.objects.all().iterator():
|
||||
allowed_tags = list(template.allowed_tags or [])
|
||||
updated_tags = list(dict.fromkeys([*allowed_tags, "customer_mobile", "category_name"]))
|
||||
if updated_tags != allowed_tags:
|
||||
template.allowed_tags = updated_tags
|
||||
template.save(update_fields=["allowed_tags"])
|
||||
|
||||
|
||||
def remove_supervisor_template(apps, schema_editor):
|
||||
apps.get_model("notifications", "SMSTemplate").objects.filter(
|
||||
unit__isnull=True, event_type=EVENT, title=TITLE, body=BODY
|
||||
).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("notifications", "0011_notification_delivery_tracking"),
|
||||
("staff", "0011_organizationalunit_supervisor_mobile_4"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="smstemplate",
|
||||
name="event_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("CUSTOMER_TICKET_CREATED", "ثبت تیکت توسط مشتری"),
|
||||
("STAFF_TICKET_CREATED", "ثبت تیکت توسط کارشناس"),
|
||||
("STAFF_REPLY", "پاسخ کارشناس"),
|
||||
(EVENT, TITLE),
|
||||
],
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(seed_supervisor_template, remove_supervisor_template),
|
||||
]
|
||||
@@ -5,7 +5,7 @@ from apps.core.models import PersianTextNormalizationMixin
|
||||
|
||||
|
||||
def sms_template_tags():
|
||||
return ["magic_link", "first_name", "last_name", "recipient_name", "unit_name", "company_name", "ticket_number", "subject", "province", "city", "expert_name"]
|
||||
return ["magic_link", "first_name", "last_name", "recipient_name", "unit_name", "company_name", "ticket_number", "subject", "province", "city", "expert_name", "customer_mobile", "category_name"]
|
||||
|
||||
class SMSTemplate(PersianTextNormalizationMixin, models.Model):
|
||||
persian_text_fields={"title":False,"body":True}
|
||||
@@ -13,6 +13,7 @@ class SMSTemplate(PersianTextNormalizationMixin, models.Model):
|
||||
CUSTOMER_TICKET_CREATED="CUSTOMER_TICKET_CREATED","ثبت تیکت توسط مشتری"
|
||||
STAFF_TICKET_CREATED="STAFF_TICKET_CREATED","ثبت تیکت توسط کارشناس"
|
||||
STAFF_REPLY="STAFF_REPLY","پاسخ کارشناس"
|
||||
UNIT_SUPERVISOR_NEW_TICKET="UNIT_SUPERVISOR_NEW_TICKET","اطلاع تیکت جدید به سرپرستان واحد"
|
||||
unit=models.ForeignKey("staff.OrganizationalUnit",on_delete=models.CASCADE,null=True,blank=True,related_name="sms_templates",help_text="خالی یعنی الگوی عمومی و fallback.")
|
||||
event_type=models.CharField(max_length=50,choices=Event.choices)
|
||||
title=models.CharField(max_length=120)
|
||||
|
||||
+2
-2
@@ -15,13 +15,13 @@ class UnitMembershipInline(admin.TabularInline):
|
||||
class OrganizationalUnitAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "slug", "bale_chat_id", "sort_order", "show_in_customer_form", "accepts_external_tickets", "is_active", "updated_at")
|
||||
list_filter = ("show_in_customer_form", "accepts_external_tickets", "is_active")
|
||||
search_fields = ("name", "slug", "bale_chat_id", "supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3")
|
||||
search_fields = ("name", "slug", "bale_chat_id", "supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3", "supervisor_mobile_4")
|
||||
list_editable = ("sort_order", "show_in_customer_form", "accepts_external_tickets", "is_active")
|
||||
filter_horizontal = ("permissions",)
|
||||
inlines = (UnitMembershipInline,)
|
||||
fieldsets = (
|
||||
("مشخصات واحد", {"fields": ("name", "slug", "description", "search_keywords", "sort_order")}),
|
||||
("اطلاعرسانی بله و سرپرستان", {"fields": ("bale_chat_id", "supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3")}),
|
||||
("اطلاعرسانی بله و سرپرستان", {"fields": ("bale_chat_id", "supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3", "supervisor_mobile_4")}),
|
||||
("نمایش و دسترسی", {"fields": ("permissions", "show_in_customer_form", "accepts_external_tickets", "is_active")}),
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("staff", "0010_organizationalunit_notification_destinations")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="organizationalunit",
|
||||
name="supervisor_mobile_4",
|
||||
field=models.CharField(blank=True, max_length=16, verbose_name="شماره همراه سرپرست چهارم"),
|
||||
),
|
||||
]
|
||||
@@ -36,6 +36,7 @@ class OrganizationalUnit(PersianTextNormalizationMixin, TimeStampedModel):
|
||||
supervisor_mobile_1 = models.CharField(max_length=16, blank=True, verbose_name="شماره همراه سرپرست اول")
|
||||
supervisor_mobile_2 = models.CharField(max_length=16, blank=True, verbose_name="شماره همراه سرپرست دوم")
|
||||
supervisor_mobile_3 = models.CharField(max_length=16, blank=True, verbose_name="شماره همراه سرپرست سوم")
|
||||
supervisor_mobile_4 = models.CharField(max_length=16, blank=True, verbose_name="شماره همراه سرپرست چهارم")
|
||||
|
||||
class Meta:
|
||||
ordering = ["sort_order", "name", "pk"]
|
||||
@@ -52,6 +53,7 @@ class OrganizationalUnit(PersianTextNormalizationMixin, TimeStampedModel):
|
||||
self.supervisor_mobile_1,
|
||||
self.supervisor_mobile_2,
|
||||
self.supervisor_mobile_3,
|
||||
self.supervisor_mobile_4,
|
||||
))))
|
||||
|
||||
def clean(self):
|
||||
@@ -60,7 +62,7 @@ class OrganizationalUnit(PersianTextNormalizationMixin, TimeStampedModel):
|
||||
|
||||
normalized = []
|
||||
errors = {}
|
||||
for field_name in ("supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3"):
|
||||
for field_name in ("supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3", "supervisor_mobile_4"):
|
||||
value = getattr(self, field_name, "").strip()
|
||||
if not value:
|
||||
setattr(self, field_name, "")
|
||||
@@ -80,7 +82,7 @@ class OrganizationalUnit(PersianTextNormalizationMixin, TimeStampedModel):
|
||||
def save(self, *args, **kwargs):
|
||||
from apps.accounts.models import normalize_mobile
|
||||
|
||||
for field_name in ("supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3"):
|
||||
for field_name in ("supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3", "supervisor_mobile_4"):
|
||||
value = getattr(self, field_name, "").strip()
|
||||
setattr(self, field_name, normalize_mobile(value) if value else "")
|
||||
self.bale_chat_id = self.bale_chat_id.strip()
|
||||
|
||||
@@ -112,6 +112,46 @@ def refresh_group_dispatch_status(dispatch_id):
|
||||
dispatch.status=status
|
||||
return dispatch
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def resolve_group_dispatch_tickets(*, dispatch, user):
|
||||
"""Resolve every non-final ticket created by one group dispatch."""
|
||||
dispatch = GroupDispatch.objects.select_for_update().select_related("unit").get(pk=dispatch.pk)
|
||||
permitted = user.has_perm("tickets.change_ticket") or TicketPolicy.has_unit_permission(
|
||||
user, dispatch.unit, "tickets.change_ticket"
|
||||
)
|
||||
if not (DispatchPolicy.can_view(user, dispatch) and permitted):
|
||||
raise PermissionDenied("اجازه حلکردن تیکتهای این ارسال گروهی را ندارید.")
|
||||
|
||||
now = timezone.now()
|
||||
tickets = list(
|
||||
dispatch.tickets.select_for_update().exclude(
|
||||
status__in=(Ticket.Status.RESOLVED, Ticket.Status.CLOSED)
|
||||
)
|
||||
)
|
||||
if not tickets:
|
||||
return 0
|
||||
previous_statuses = {ticket.pk: ticket.status for ticket in tickets}
|
||||
for ticket in tickets:
|
||||
ticket.status = Ticket.Status.RESOLVED
|
||||
ticket.resolved_at = now
|
||||
ticket.updated_at = now
|
||||
Ticket.objects.bulk_update(tickets, ["status", "resolved_at", "updated_at"])
|
||||
TicketEvent.objects.bulk_create([
|
||||
TicketEvent(
|
||||
ticket=ticket,
|
||||
event_type=TicketEvent.Type.STATUS_CHANGED,
|
||||
actor=user,
|
||||
metadata={
|
||||
"from": previous_statuses[ticket.pk],
|
||||
"to": Ticket.Status.RESOLVED,
|
||||
"group_dispatch_id": dispatch.pk,
|
||||
},
|
||||
)
|
||||
for ticket in tickets
|
||||
])
|
||||
return len(tickets)
|
||||
|
||||
@transaction.atomic
|
||||
def claim_ticket(ticket_id,user):
|
||||
ticket=Ticket.objects.select_for_update().get(pk=ticket_id)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
app_name="staff_tickets"
|
||||
urlpatterns=[path("",views.staff_list,name="list"),path("customers/",views.staff_customers,name="customers"),path("customers/search/",views.staff_customer_search,name="customer_search"),path("contacts/",views.staff_contacts,name="contacts"),path("contacts/new/",views.staff_contact_create,name="contact_create"),path("contacts/<uuid:public_id>/edit/",views.staff_contact_edit,name="contact_edit"),path("contact-groups/",views.staff_groups,name="groups"),path("contact-groups/<int:group_id>/",views.staff_groups,name="group_edit"),path("magic-links/",views.staff_magic_audit,name="magic_audit"),path("magic-links/<int:link_id>/revoke/",views.staff_magic_revoke,name="magic_revoke"),path("sms-templates/",views.staff_sms_templates,name="sms_templates"),path("sms-templates/<int:template_id>/",views.staff_sms_templates,name="sms_template_edit"),path("dispatches/",views.staff_dispatch_create,name="dispatch_create"),path("dispatches/<uuid:public_id>/",views.staff_dispatch_detail,name="dispatch_detail"),path("dispatches/<uuid:public_id>/retry/",views.staff_dispatch_retry,name="dispatch_retry"),path("experts/<uuid:public_id>/avatar/",views.expert_avatar,name="expert_avatar"),path("tickets/",views.staff_list,name="tickets"),path("tickets/new/",views.staff_create,name="create"),path("tickets/<uuid:public_id>/messages/older/",views.staff_messages,name="messages"),path("tickets/<uuid:public_id>/messages/poll/",views.staff_poll,name="poll"),path("tickets/<uuid:public_id>/",views.staff_detail,name="detail"),path("tickets/<uuid:public_id>/claim/",views.staff_claim,name="claim"),path("tickets/<uuid:public_id>/reply/",views.staff_reply,name="reply"),path("tickets/<uuid:public_id>/note/",lambda r,public_id:views.staff_reply(r,public_id,note=True),name="note"),path("tickets/<uuid:public_id>/transfer/",views.staff_transfer,name="transfer"),path("tickets/<uuid:public_id>/share/",views.staff_share,name="share"),path("tickets/<uuid:public_id>/revoke/<uuid:user_id>/",views.staff_revoke,name="revoke"),path("tickets/<uuid:public_id>/status/",views.staff_status,name="status"),path("tickets/<uuid:public_id>/priority/",views.staff_priority,name="priority"),path("tickets/<uuid:public_id>/delete/",views.staff_delete,name="delete")]
|
||||
urlpatterns=[path("",views.staff_list,name="list"),path("customers/",views.staff_customers,name="customers"),path("customers/search/",views.staff_customer_search,name="customer_search"),path("contacts/",views.staff_contacts,name="contacts"),path("contacts/new/",views.staff_contact_create,name="contact_create"),path("contacts/<uuid:public_id>/edit/",views.staff_contact_edit,name="contact_edit"),path("contact-groups/",views.staff_groups,name="groups"),path("contact-groups/<int:group_id>/",views.staff_groups,name="group_edit"),path("magic-links/",views.staff_magic_audit,name="magic_audit"),path("magic-links/<int:link_id>/revoke/",views.staff_magic_revoke,name="magic_revoke"),path("sms-templates/",views.staff_sms_templates,name="sms_templates"),path("sms-templates/<int:template_id>/",views.staff_sms_templates,name="sms_template_edit"),path("dispatches/",views.staff_dispatch_create,name="dispatch_create"),path("dispatches/<uuid:public_id>/",views.staff_dispatch_detail,name="dispatch_detail"),path("dispatches/<uuid:public_id>/retry/",views.staff_dispatch_retry,name="dispatch_retry"),path("dispatches/<uuid:public_id>/resolve/",views.staff_dispatch_resolve,name="dispatch_resolve"),path("experts/<uuid:public_id>/avatar/",views.expert_avatar,name="expert_avatar"),path("tickets/",views.staff_list,name="tickets"),path("tickets/new/",views.staff_create,name="create"),path("tickets/<uuid:public_id>/messages/older/",views.staff_messages,name="messages"),path("tickets/<uuid:public_id>/messages/poll/",views.staff_poll,name="poll"),path("tickets/<uuid:public_id>/",views.staff_detail,name="detail"),path("tickets/<uuid:public_id>/claim/",views.staff_claim,name="claim"),path("tickets/<uuid:public_id>/reply/",views.staff_reply,name="reply"),path("tickets/<uuid:public_id>/note/",lambda r,public_id:views.staff_reply(r,public_id,note=True),name="note"),path("tickets/<uuid:public_id>/transfer/",views.staff_transfer,name="transfer"),path("tickets/<uuid:public_id>/share/",views.staff_share,name="share"),path("tickets/<uuid:public_id>/revoke/<uuid:user_id>/",views.staff_revoke,name="revoke"),path("tickets/<uuid:public_id>/status/",views.staff_status,name="status"),path("tickets/<uuid:public_id>/priority/",views.staff_priority,name="priority"),path("tickets/<uuid:public_id>/delete/",views.staff_delete,name="delete")]
|
||||
+24
-4
@@ -31,7 +31,7 @@ from .forms import ContactGroupForm,GroupDispatchForm,ReplyForm,ShareForm,StaffC
|
||||
from .models import GroupDispatch,Ticket,TicketCategory,TicketReadState,TicketTag
|
||||
from .policies import DispatchPolicy, TicketPolicy
|
||||
from .selectors import customer_tickets,dashboard_counts,filter_staff_tickets,staff_ticket_rows
|
||||
from .services import change_priority,change_status,claim_ticket,create_ticket,dispatch_group_tickets,refresh_group_dispatch_status,reply_ticket,revoke_access,share_ticket,transfer_ticket
|
||||
from .services import change_priority,change_status,claim_ticket,create_ticket,dispatch_group_tickets,refresh_group_dispatch_status,reply_ticket,resolve_group_dispatch_tickets,revoke_access,share_ticket,transfer_ticket
|
||||
MESSAGE_PAGE_SIZE=50
|
||||
def conversation_page(queryset,before=None):
|
||||
queryset=queryset.order_by("-created_at","-id")
|
||||
@@ -389,7 +389,7 @@ def staff_customer_search(request):
|
||||
return JsonResponse({"results":results})
|
||||
@staff_required
|
||||
def staff_detail(request,public_id):
|
||||
ticket=get_object_or_404(Ticket.objects.select_related("customer__customer_profile","category","owner__staff_profile","selected_expert__staff_profile"),public_id=public_id)
|
||||
ticket=get_object_or_404(Ticket.objects.select_related("customer__customer_profile","category","owner__staff_profile","selected_expert__staff_profile","group_dispatch__unit","group_dispatch__contact_group"),public_id=public_id)
|
||||
if not TicketPolicy.can_view(request.user,ticket): raise PermissionDenied
|
||||
conversation,has_older,older_cursor=conversation_page(ticket.ticket_messages.select_related("author").prefetch_related("attachments","read_receipts__user__staff_profile","read_receipts__user__customer_profile"))
|
||||
mark_messages_read(conversation,request.user)
|
||||
@@ -397,7 +397,10 @@ def staff_detail(request,public_id):
|
||||
history=Ticket.objects.filter(customer=ticket.customer).exclude(pk=ticket.pk).select_related("owner__staff_profile","selected_expert__staff_profile").order_by("-last_activity_at")[:5]
|
||||
sms_jobs=ticket.notificationjob_set.filter(channel=NotificationJob.Channel.SMS).exclude(event_type="UNIT_SUPERVISOR_NEW_TICKET").order_by("-created_at")
|
||||
latest=conversation[-1] if conversation else None
|
||||
return render(request,"tickets/staff_detail.html",{"ticket":ticket,"conversation":conversation,"has_older":has_older,"older_cursor":older_cursor,"form":StaffReplyForm(),"transfer_form":TransferForm(),"share_form":ShareForm(),"can_claim":TicketPolicy.can_claim(request.user,ticket),"can_transfer":TicketPolicy.can_transfer(request.user,ticket),"can_share":TicketPolicy.can_share(request.user,ticket),"can_delete":TicketPolicy.can_delete(request.user,ticket),"statuses":Ticket.Status.choices,"priorities":Ticket.Priority.choices,"grants":ticket.access_grants.filter(revoked_at__isnull=True).select_related("user__staff_profile"),"customer_history":history,"sms_jobs":sms_jobs,"viewer_user":request.user,"poll_version":conversation_version(ticket,latest),"poll_cursor":latest.public_id if latest else ""})
|
||||
group_dispatch = ticket.group_dispatch
|
||||
can_resolve_dispatch = bool(group_dispatch and DispatchPolicy.can_view(request.user, group_dispatch) and (request.user.has_perm("tickets.change_ticket") or TicketPolicy.has_unit_permission(request.user, group_dispatch.unit, "tickets.change_ticket")))
|
||||
group_open_count = group_dispatch.tickets.exclude(status__in=(Ticket.Status.RESOLVED, Ticket.Status.CLOSED)).count() if group_dispatch else 0
|
||||
return render(request,"tickets/staff_detail.html",{"ticket":ticket,"conversation":conversation,"has_older":has_older,"older_cursor":older_cursor,"form":StaffReplyForm(),"transfer_form":TransferForm(),"share_form":ShareForm(),"can_claim":TicketPolicy.can_claim(request.user,ticket),"can_transfer":TicketPolicy.can_transfer(request.user,ticket),"can_share":TicketPolicy.can_share(request.user,ticket),"can_delete":TicketPolicy.can_delete(request.user,ticket),"can_resolve_dispatch":can_resolve_dispatch,"group_open_count":group_open_count,"statuses":Ticket.Status.choices,"priorities":Ticket.Priority.choices,"grants":ticket.access_grants.filter(revoked_at__isnull=True).select_related("user__staff_profile"),"customer_history":history,"sms_jobs":sms_jobs,"viewer_user":request.user,"poll_version":conversation_version(ticket,latest),"poll_cursor":latest.public_id if latest else ""})
|
||||
@staff_required
|
||||
def staff_messages(request,public_id):
|
||||
ticket=get_object_or_404(Ticket,public_id=public_id)
|
||||
@@ -612,7 +615,24 @@ def staff_dispatch_detail(request, public_id):
|
||||
pending = sum(sms_counts.get(status, 0) for status in (NotificationJob.Status.PENDING, NotificationJob.Status.PROCESSING, NotificationJob.Status.RETRYING))
|
||||
delivery_percent = round((delivered / dispatch.recipient_count) * 100) if dispatch.recipient_count else 0
|
||||
metrics = {"sent": sent, "delivered": delivered, "failed": failed, "retrying": retrying, "tracking": tracking, "pending": pending, "opened": opened, "not_opened": max(dispatch.recipient_count-opened,0), "total_views": total_views, "replied": replied, "delivery_percent": delivery_percent}
|
||||
return render(request, "tickets/staff_dispatch_detail.html", {"dispatch": dispatch, "tickets": tickets, "metrics": metrics, "sms_max_attempts": 1 + settings.SMS_MAX_RETRIES, "active_staff_section": "dispatches"})
|
||||
open_ticket_count = sum(ticket.status not in (Ticket.Status.RESOLVED, Ticket.Status.CLOSED) for ticket in tickets)
|
||||
can_resolve_dispatch = request.user.has_perm("tickets.change_ticket") or TicketPolicy.has_unit_permission(request.user, dispatch.unit, "tickets.change_ticket")
|
||||
return render(request, "tickets/staff_dispatch_detail.html", {"dispatch": dispatch, "tickets": tickets, "metrics": metrics, "open_ticket_count": open_ticket_count, "can_resolve_dispatch": can_resolve_dispatch, "sms_max_attempts": 1 + settings.SMS_MAX_RETRIES, "active_staff_section": "dispatches"})
|
||||
|
||||
|
||||
@staff_required
|
||||
@require_POST
|
||||
def staff_dispatch_resolve(request, public_id):
|
||||
dispatch = get_object_or_404(GroupDispatch, public_id=public_id)
|
||||
try:
|
||||
count = resolve_group_dispatch_tickets(dispatch=dispatch, user=request.user)
|
||||
messages.success(request, f"{count} تیکت این ارسال گروهی حلشده علامتگذاری شد." if count else "همه تیکتهای این ارسال گروهی از قبل حل یا بسته شدهاند.")
|
||||
except PermissionDenied as exc:
|
||||
messages.error(request, str(exc))
|
||||
return_ticket = request.POST.get("return_ticket", "").strip()
|
||||
if return_ticket and dispatch.tickets.filter(public_id=return_ticket).exists():
|
||||
return redirect("staff_tickets:detail", public_id=return_ticket)
|
||||
return redirect("staff_tickets:dispatch_detail", public_id=public_id)
|
||||
|
||||
|
||||
@staff_required
|
||||
|
||||
@@ -14,3 +14,4 @@
|
||||
.ticket-review-callout{display:flex;align-items:center;gap:.65rem;margin:.85rem 0 .65rem;border-radius:.9rem;background:#f0faf6;padding:.7rem}.ticket-review-callout>span{display:grid;width:2rem;height:2rem;flex:none;place-items:center;border-radius:.65rem;background:#fff;color:#16815e;font-weight:900}.ticket-review-callout b,.ticket-review-callout small{display:block}.ticket-review-callout b{font-size:.82rem}.ticket-review-callout small{margin-top:.1rem;color:#66877b;font-size:.7rem;line-height:1.65}.ticket-review-list{overflow:hidden;border:1px solid rgba(8,55,107,.1);border-radius:1rem;background:#fff}.ticket-review-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:start;gap:.6rem;padding:.75rem}.ticket-review-row+.ticket-review-row{border-top:1px solid rgba(8,55,107,.07)}.ticket-review-icon{display:grid;width:1.8rem;height:1.8rem;place-items:center;border-radius:.55rem;background:#e5f4ff;color:#1977ce;font-size:.65rem;font-weight:900}.ticket-review-content{min-width:0}.ticket-review-content>small{color:rgba(8,55,107,.58);font-size:.7rem;font-weight:800}.ticket-review-content h3{overflow-wrap:anywhere;margin:.16rem 0 .08rem;color:#08376b;font-size:.9rem;line-height:1.7}.ticket-review-content [data-review-subject]{font-size:1rem;font-weight:900;line-height:1.8}.ticket-review-content p{display:flex;flex-wrap:wrap;align-items:center;gap:.3rem;margin:0;color:rgba(8,55,107,.62);font-size:.72rem;line-height:1.8}.ticket-review-content p i{font-style:normal}.ticket-review-row>button{display:flex;min-height:2rem;align-items:center;gap:.25rem;border:0;border-radius:.65rem;background:#e5f4ff;padding:.35rem .5rem;color:#1977ce;font-size:.68rem;font-weight:900}.ticket-review-details .ticket-review-content{grid-column:2/-1}.ticket-review-details>button{grid-column:3;grid-row:1}.ticket-review-body{display:block!important;margin-top:.2rem!important;color:rgba(8,55,107,.82)!important;font-size:.88rem!important;font-weight:550;line-height:2!important;white-space:pre-wrap}.ticket-review-files{display:flex;flex-wrap:wrap;gap:.3rem;margin-top:.45rem}.ticket-review-files span{border-radius:999px;background:#e5f4ff;padding:.28rem .5rem;color:#1977ce;font-size:.68rem}.ticket-review-audio{margin-top:.55rem}.ticket-review-step{padding-bottom:6.75rem}.ticket-wizard-card[data-wizard-enhanced] .ticket-final-actions{position:fixed;z-index:45;right:.75rem;bottom:5rem;left:.75rem;display:grid;grid-template-columns:minmax(0,.78fr) minmax(0,1.4fr);gap:.5rem;margin:0;border:1px solid rgba(8,55,107,.12);border-radius:1.1rem;background:rgba(255,255,255,.96);padding:.55rem;box-shadow:0 20px 50px -20px rgba(8,55,107,.55);backdrop-filter:blur(14px)}
|
||||
@media(min-width:640px){.ticket-wizard-page{padding-bottom:3rem}.ticket-wizard-topbar{min-height:3.8rem}.ticket-wizard-save{display:flex}.ticket-wizard-intro{margin:.8rem 0 1.35rem}.ticket-wizard-intro h1{font-size:2rem}.ticket-wizard-intro>p:last-child{font-size:.9rem}.ticket-wizard-progress{margin-bottom:1rem}.ticket-wizard-progress:before{top:1.12rem}.ticket-wizard-progress li{display:flex;justify-content:center;gap:.5rem}.ticket-wizard-progress li>span{width:2.3rem;height:2.3rem}.ticket-wizard-progress li>div{display:block}.ticket-wizard-progress li b,.ticket-wizard-progress li small{display:block}.ticket-wizard-progress li b{font-size:.78rem}.ticket-wizard-progress li small{font-size:.67rem}.ticket-wizard-card{border-radius:1.6rem}.ticket-wizard-step{padding:1.35rem 1.5rem}.ticket-wizard-step-head h2{font-size:1.2rem}.ticket-intent-search kbd{display:inline;border:1px solid rgba(8,55,107,.1);border-radius:.4rem;background:#eee;padding:.2rem .4rem;color:rgba(8,55,107,.58);font-size:.68rem}.ticket-routing-grid,.ticket-two-columns{grid-template-columns:1fr 1fr}.ticket-manual-body{padding:1rem!important}.ticket-expert-picker{grid-template-columns:repeat(3,1fr)!important}.ticket-expert-picker .expert-card{display:grid!important;grid-template-columns:1fr;justify-items:center;padding:.9rem!important;text-align:center!important}.ticket-expert-picker .expert-card img,.ticket-expert-picker .expert-avatar{width:3.3rem!important;height:3.3rem!important}.ticket-expert-picker .expert-card:after{position:absolute;top:.5rem;left:.5rem}.ticket-attachment-actions{grid-template-columns:1fr 1fr}.ticket-voice-idle{grid-template-columns:auto 1fr;align-items:center}.ticket-voice-idle>small{text-align:right}.ticket-voice-start{min-width:11rem}.ticket-voice-live{display:grid;grid-template-columns:1fr auto;align-items:center}.ticket-voice-controls{grid-template-columns:auto auto auto}.ticket-voice-controls .ticket-voice-cancel{grid-column:auto;min-height:2.65rem}.ticket-wizard-actions{position:static;margin:1.15rem -1.5rem -1.35rem;padding:.8rem 1.5rem}.ticket-wizard-actions>span{display:block;margin-left:auto;color:rgba(8,55,107,.45);font-size:.72rem}.ticket-wizard-actions button{min-width:8.5rem}.ticket-review-row{gap:.8rem;padding:.9rem 1rem}.ticket-review-details .ticket-review-content{grid-column:2}.ticket-review-content h3{font-size:.96rem}.ticket-review-content [data-review-subject]{font-size:1.08rem}.ticket-review-body{font-size:.94rem!important}.ticket-review-step{padding-bottom:6rem}.ticket-wizard-card[data-wizard-enhanced] .ticket-final-actions{position:fixed;right:auto;bottom:1rem;left:50%;width:min(calc(100% - 2rem),42rem);transform:translateX(-50%);padding:.6rem}}
|
||||
@media(prefers-reduced-motion:reduce){.ticket-voice-wave i{animation:none}}
|
||||
.ticket-wizard-credit{margin-top:-1px;border:1px solid rgba(8,55,107,.1);border-top:1px dashed rgba(8,55,107,.13);border-radius:0 0 1.15rem 1.15rem;background:#f8fbfd;padding:.85rem 1rem;color:rgba(8,55,107,.62);font-size:.74rem;line-height:1.9;text-align:center}.ticket-wizard-credit a{color:#1977ce}.ticket-wizard-card{border-bottom-right-radius:1.15rem;border-bottom-left-radius:1.15rem}@media(min-width:640px){.ticket-wizard-credit{border-radius:0 0 1.6rem 1.6rem}.ticket-wizard-card{border-bottom-right-radius:1.6rem;border-bottom-left-radius:1.6rem}}
|
||||
@@ -17,3 +17,6 @@
|
||||
/* Shared attachments for isolated group dispatches. */
|
||||
.dispatch-attachment-picker{display:grid;gap:.8rem;margin:0 1rem 1rem;border:1px dashed #8fb7d1;border-radius:.9rem;background:linear-gradient(145deg,#f8fcff,#f2f9fd);padding:1rem}.dispatch-attachment-picker .attachment-picker-copy{display:flex;align-items:center;gap:.75rem}.dispatch-attachment-picker .attachment-icon{display:grid;width:2.45rem;height:2.45rem;flex:none;place-items:center;border-radius:.72rem;background:#dff2ff;color:#1977ce;font-size:1.2rem;font-weight:900}.dispatch-attachment-picker b,.dispatch-attachment-picker small{display:block}.dispatch-attachment-picker b{color:#173f60;font-size:.78rem}.dispatch-attachment-picker .attachment-picker-copy small{margin-top:.15rem;color:#647d91;font-size:.65rem;line-height:1.7}.dispatch-attachment-picker .media-picker-actions{margin:0}.dispatch-attachment-picker .file-preview{margin-top:0}.dispatch-attachment-picker .file-preview:empty{display:none}.dispatch-attachment-picker .file-chip{box-shadow:0 10px 25px -22px #08376b}.dispatch-attachment-picker .compression-note{color:#688095;font-size:.62rem;line-height:1.7}
|
||||
@media(max-width:700px){.dispatch-attachment-picker{margin:.8rem;padding:.8rem}.dispatch-attachment-picker .media-picker-actions{display:grid;grid-template-columns:1fr}.dispatch-attachment-picker .media-picker-button{justify-content:center}}
|
||||
|
||||
.dispatch-resolve-button{border:1px solid #86d5b4!important;background:#eaf9f2!important;color:#126747!important}.dispatch-resolution-warning{display:flex;align-items:flex-start;gap:.75rem;border:1px solid #f0cf84;border-radius:1rem;background:#fffaf0;padding:.9rem 1rem;color:#6e4b0a}.dispatch-resolution-warning>span{display:grid;width:1.8rem;height:1.8rem;flex:none;place-items:center;border-radius:.55rem;background:#f5b942;color:#fff;font-weight:900}.dispatch-resolution-warning b{font-size:.82rem}.dispatch-resolution-warning p{margin:.2rem 0 0;font-size:.72rem;line-height:1.8}.ticket-dispatch-context{position:relative;display:flex;align-items:center;justify-content:space-between;gap:.65rem;border:1px solid #d4e7f0;border-radius:.75rem;background:#f8fcfe;padding:.55rem 2.65rem .55rem .7rem}.ticket-dispatch-context>div:first-child{display:flex;align-items:center;gap:.55rem;min-width:0}.ticket-dispatch-context>div:first-child>span{display:grid;width:1.75rem;height:1.75rem;flex:none;place-items:center;border-radius:.5rem;background:#e3f3fa;color:#0877a7;font-size:.78rem;font-weight:900}.ticket-dispatch-context p{display:grid;min-width:0;gap:.03rem;margin:0}.ticket-dispatch-context small,.ticket-dispatch-context em{color:#718695;font-size:.6rem;font-style:normal}.ticket-dispatch-context b{overflow:hidden;color:#173f60;font-size:.76rem;text-overflow:ellipsis;white-space:nowrap}.ticket-dispatch-context-actions{display:flex;align-items:center;gap:.4rem}.ticket-dispatch-context-actions form{margin:0}.ticket-dispatch-context-actions a,.ticket-dispatch-context-actions button{display:inline-flex;min-height:2rem;align-items:center;justify-content:center;border:1px solid #c8e0ea;border-radius:.55rem;background:#fff;padding:.35rem .55rem;color:#176b91;font:inherit;font-size:.62rem;font-weight:900;cursor:pointer}.ticket-dispatch-context-actions button{border-color:#a9dec9;background:#effaf5;color:#126747}.ticket-dispatch-context-close{position:absolute;top:50%;right:.55rem;display:grid;width:1.65rem;height:1.65rem;min-height:0;transform:translateY(-50%);place-items:center;border:0;border-radius:50%;background:transparent;padding:0;color:#718695;font:inherit;font-size:1.15rem;line-height:1;box-shadow:none;cursor:pointer}.ticket-dispatch-context-close:hover{transform:translateY(-50%);background:#e7f1f5;color:#173f60}
|
||||
@media(max-width:760px){.dispatch-head-actions>*{width:100%}.dispatch-head-actions .button{width:100%;justify-content:center}.ticket-dispatch-context{align-items:stretch;flex-direction:column;padding:.5rem 2.45rem .5rem .55rem}.ticket-dispatch-context-actions{display:flex;overflow-x:auto}.ticket-dispatch-context-actions a,.ticket-dispatch-context-actions form{flex:1}.ticket-dispatch-context-actions a,.ticket-dispatch-context-actions button{width:100%;white-space:nowrap}.ticket-dispatch-context-close{top:.5rem;transform:none}.ticket-dispatch-context-close:hover{transform:none}}
|
||||
@@ -1,2 +1,2 @@
|
||||
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-tracking:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:Vazirmatn,ui-sans-serif,system-ui;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-800:oklch(47.3% .137 46.201);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-800:oklch(43.2% .095 166.913);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-400:oklch(70.4% .04 256.788);--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-normal:400;--font-weight-medium:500;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--blur-sm:8px;--blur-xl:24px;--blur-2xl:40px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-ink:#08376b;--color-ink-soft:#0b4c8c;--color-cream:#eee;--color-soft:#eaeaea;--color-paper:#fff;--color-sea:#0693e3;--color-sea-dark:#1977ce;--color-blue:#1977ce;--color-mint:#e5f4ff;--color-coral:#fa9f00;--color-sun:#ffc451}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--defLine truncated
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-leading:initial;--tw-font-weight:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-tracking:initial;--tw-border-style:solid;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:Vazirmatn,ui-sans-serif,system-ui;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-800:oklch(47.3% .137 46.201);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-800:oklch(43.2% .095 166.913);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-400:oklch(70.4% .04 256.788);--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-normal:400;--font-weight-medium:500;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--blur-sm:8px;--blur-xl:24px;--blur-2xl:40px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-ink:#08376b;--color-ink-soft:#0b4c8c;--color-cream:#eee;--color-soft:#eaeaea;--color-paper:#fff;--color-sea:#0693e3;--color-sea-dark:#1977ce;--color-blue:#1977ce;--color-mint:#e5f4ff;--color-coral:#fa9f00;--color-sun:#ffc451;--shadow-panel:0 18px 55px -35px #08376b59}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-Line truncated
|
||||
@@ -44,6 +44,7 @@ document.querySelectorAll('[data-conversation-workspace]').forEach(workspace=>{
|
||||
if(submissionSucceeded){form?.reset();if(input){input.value='';resize()}setComposerOpen(false,false)}else setComposerOpen(hasDraft||replyResult==='error',false);
|
||||
composerToggle?.addEventListener('click',()=>setComposerOpen(true));
|
||||
workspace.querySelectorAll('[data-composer-close]').forEach(button=>button.addEventListener('click',()=>setComposerOpen(false,false)));
|
||||
workspace.querySelector('[data-dismiss-dispatch-context]')?.addEventListener('click',event=>event.currentTarget.closest('.ticket-dispatch-context')?.remove());
|
||||
composer?.addEventListener('keydown',event=>{if(event.key==='Escape'){setComposerOpen(false,false);composerToggle?.focus()}});
|
||||
const primarySubmit=form?.querySelector('[data-primary-submit]');
|
||||
const guideToPrimarySubmit=event=>{event.preventDefault();event.stopPropagation();if(!primarySubmit)return;primarySubmit.scrollIntoView({behavior:'smooth',block:'center'});primarySubmit.classList.remove('submit-attention');void primarySubmit.offsetWidth;primarySubmit.classList.add('submit-attention');setTimeout(()=>primarySubmit.focus({preventScroll:true}),450)};
|
||||
|
||||
@@ -41,9 +41,13 @@ body:has(.ticket-workspace){overflow:hidden}.customer-main:has(.ticket-workspace
|
||||
|
||||
/* Collapsible reply panel keeps the correspondence history dominant. */
|
||||
.ticket-workspace .composer.is-collapsed{padding:.55rem .85rem;border-top:1px solid rgba(8,55,107,.1);background:var(--color-paper)}.composer.is-collapsed>form,.composer.is-collapsed>.mode{display:none}.composer-launch{display:none;width:100%;min-height:3.5rem;align-items:center;justify-content:flex-start;gap:.75rem;border:1px solid rgba(8,55,107,.1);border-radius:11px;background:linear-gradient(135deg,var(--color-paper),var(--color-mint));padding:.55rem .75rem;color:var(--color-ink);box-shadow:none;text-align:right}.composer.is-collapsed .composer-launch{display:flex}.composer-launch:hover{transform:none;border-color:color-mix(in srgb,var(--color-sea) 35%,transparent);background:var(--color-mint)}.composer-launch-icon{@apply grid size-9 shrink-0 place-items-center rounded-lg bg-sea text-lg text-white}.composer-launch b,.composer-launch small{display:block}.composer-launch b{@apply text-xs}.composer-launch small{@apply mt-0.5 text-[9px] font-medium text-ink/45}.composer-launch-arrow{@apply mr-auto text-sm text-sea}.composer-close{@apply text-ink/55}.composer:not(.is-collapsed) .composer-launch{display:none}.composer.is-collapsed~*{transition:none}.composer.is-collapsed+.new-message-jump{bottom:5rem}.ticket-workspace .composer.is-collapsed{box-shadow:0 -10px 30px -28px rgba(8,55,107,.65)}
|
||||
@media(max-width:639px){.ticket-workspace .composer.is-collapsed{@apply px-2.5 py-2}.composer-launch{min-height:3.25rem}.composer-launch small{max-width:65vw}.composer-close{order:3}.new-message-jump{bottom:5.5rem}.ticket-workspace .composer:not(.is-collapsed){max-height:62dvh;overflow-y:auto}}
|
||||
@media(max-width:639px){.ticket-workspace .composer.is-collapsed{@apply px-2.5 py-2}.composer-launch{min-height:3.25rem}.composer-launch small{max-width:65vw}.new-message-jump{bottom:5.5rem}.staff-ticket-workspace .composer:not(.is-collapsed){position:fixed;inset-inline:0;bottom:0;z-index:55;box-sizing:border-box;max-height:calc(100dvh - .5rem);overflow-y:auto;overscroll-behavior:contain;border-radius:1rem 1rem 0 0;padding-bottom:max(1rem,env(safe-area-inset-bottom));box-shadow:0 -18px 55px -25px rgba(8,55,107,.75)}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-bar{position:sticky;bottom:calc(-1 * max(1rem,env(safe-area-inset-bottom)));z-index:2;margin-inline:-.65rem;margin-bottom:calc(-1 * max(1rem,env(safe-area-inset-bottom)));padding:.75rem .65rem max(1rem,env(safe-area-inset-bottom));background:var(--color-paper)}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-tools{display:grid;grid-template-columns:1fr 1fr 1fr;width:100%}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-tools .tool-button{justify-content:center;min-width:0;padding-inline:.4rem}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-close{display:inline-flex;order:initial;color:var(--color-ink)} }
|
||||
.conversation-pane:has(.composer.is-collapsed) .new-message-jump{bottom:5rem}.conversation-pane:not(:has(.composer.is-collapsed)) .new-message-jump{bottom:17rem}@media(max-width:639px){.conversation-pane:has(.composer.is-collapsed) .new-message-jump{bottom:5.5rem}.conversation-pane:not(:has(.composer.is-collapsed)) .new-message-jump{bottom:19rem}}
|
||||
body:has(.ticket-workspace) .flash{position:fixed;top:5rem;left:50%;z-index:100;width:min(calc(100% - 2rem),42rem);margin:0;transform:translateX(-50%);box-shadow:0 18px 50px -25px rgba(8,55,107,.55);animation:flash-in .22s ease-out}.flash{position:relative;padding-left:3rem}.flash-close{position:absolute;left:.5rem;top:50%;min-height:2rem;width:2rem;transform:translateY(-50%);border-radius:.5rem;background:transparent;padding:0;color:currentColor;box-shadow:none}.flash-close:hover{transform:translateY(-50%);background:rgba(255,255,255,.6)}.flash-leaving{opacity:0;transform:translate(-50%,-.5rem)!important;transition:.25s ease}@keyframes flash-in{from{opacity:0;transform:translate(-50%,-.5rem)}to{opacity:1;transform:translate(-50%,0)}}
|
||||
|
||||
/* Group context adds a row above the conversation; keep every control inside the viewport. */
|
||||
.staff-ticket-workspace:has(.ticket-dispatch-context){display:grid;grid-template-rows:auto auto minmax(0,1fr)}.staff-ticket-workspace:has(.ticket-dispatch-context) .workspace-grid{height:auto}.ticket-dispatch-context{margin:.5rem .65rem 0}
|
||||
@media(max-width:639px){.staff-ticket-workspace{min-width:0}.ticket-workspace-head{min-width:0}.ticket-title{flex:1}.ticket-head-actions{gap:.35rem}.ticket-head-actions .icon-button{width:2.35rem;height:2.35rem}.ticket-dispatch-context{margin:.4rem .45rem 0}.composer-launch{min-width:0;padding-inline:.55rem;gap:.5rem}.composer-launch>span:nth-child(2){min-width:0;flex:1}.composer-launch b,.composer-launch small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:none}.composer-launch-arrow{flex:none;margin-right:0}.staff-ticket-workspace .composer.is-collapsed{position:fixed;right:0;bottom:0;left:0;z-index:45;padding-bottom:max(.5rem,env(safe-area-inset-bottom))}.staff-ticket-workspace .conversation{padding-bottom:5.25rem}}
|
||||
.flash.flash-leaving{transform:none!important}body:has(.ticket-workspace) .flash.flash-leaving{transform:translate(-50%,-.5rem)!important}
|
||||
|
||||
/* Searchable Iran province/city fields shared by profile and new-ticket forms. */
|
||||
|
||||
@@ -46,11 +46,13 @@
|
||||
<div class="intent-results ticket-intent-results" data-ticket-search-results aria-live="polite"></div>
|
||||
|
||||
<details class="ticket-manual-routing manual-routing" {% if form.is_bound and form.errors %}open{% endif %}>
|
||||
<summary><span>انتخاب دستی واحد و دستهبندی</span><small>در صورت تمایل مسیر درخواست را خودتان مشخص کنید</small></summary>
|
||||
<summary><span>موضوع را بهصورت دستی مسیردهی کنید</span><small>واحد و دستهبندی درخواست را خودتان مشخص کنید</small></summary>
|
||||
<div class="ticket-manual-body manual-routing-body">
|
||||
<div class="ticket-routing-grid" data-routing-selectors>
|
||||
<div class="field">{{ form.unit.label_tag }}{{ form.unit }}{{ form.unit.errors }}</div>
|
||||
<div class="field">{{ form.category.label_tag }}{{ form.category }}{{ form.category.errors }}</div>
|
||||
<div class="routing-selectors" data-routing-selectors>
|
||||
<div class="ticket-routing-grid">
|
||||
<div class="field">{{ form.unit.label_tag }}{{ form.unit }}{{ form.unit.errors }}</div>
|
||||
<div class="field">{{ form.category.label_tag }}{{ form.category }}{{ form.category.errors }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
@@ -135,6 +137,7 @@
|
||||
<footer class="ticket-wizard-actions ticket-final-actions"><button class="secondary" type="button" data-wizard-prev>مرحله قبل</button><button class="primary" type="submit" data-final-submit>ثبت و ارسال نهایی <b>←</b></button></footer>
|
||||
</section>
|
||||
</form>
|
||||
<footer class="ticket-wizard-credit">{{ developer_credit }}</footer>
|
||||
</div>
|
||||
|
||||
<script src="{% static 'vendor/compressor.min.js' %}?v={{ static_asset_version }}"></script>
|
||||
@@ -144,3 +147,4 @@
|
||||
<script src="{% static 'js/attachment-uploader.js' %}?v={{ static_asset_version }}"></script>
|
||||
<script src="{% static 'js/customer-ticket-wizard.js' %}?v={{ static_asset_version }}"></script>
|
||||
{% endblock %}
|
||||
{% block footer %}{% endblock %}
|
||||
@@ -6,6 +6,13 @@
|
||||
<div class="ticket-title"><a class="workspace-back" href="{% url 'staff_tickets:list' %}" aria-label="بازگشت">→</a><div><span class="ticket-number" dir="ltr">{{ ticket.ticket_number }}</span><h1>{{ ticket.subject }}</h1></div></div>
|
||||
<div class="ticket-head-actions"><span class="badge" data-ticket-status>{{ ticket.get_status_display }}</span>{% if can_claim %}<form method="post" action="{% url 'staff_tickets:claim' ticket.public_id %}">{% csrf_token %}<button type="submit">اختصاص به من</button></form>{% endif %}<button type="button" class="icon-button" data-toggle-panel="ticket-actions" aria-label="عملیات تیکت">⚙</button><button type="button" class="icon-button" data-toggle-panel="customer-info" aria-label="اطلاعات مشتری">◉</button></div>
|
||||
</header>
|
||||
{% if ticket.group_dispatch %}
|
||||
<section class="ticket-dispatch-context" aria-label="اطلاعات ارسال گروهی">
|
||||
<div><span aria-hidden="true">⇶</span><p><small>ارسال گروهی</small><b>{{ ticket.group_dispatch.subject }}</b><em>{{ ticket.group_dispatch.contact_group.name }}</em></p></div>
|
||||
<div class="ticket-dispatch-context-actions"><a href="{% url 'staff_tickets:dispatch_detail' ticket.group_dispatch.public_id %}">مشاهده گزارش گروه</a>{% if can_resolve_dispatch and group_open_count %}<form method="post" action="{% url 'staff_tickets:dispatch_resolve' ticket.group_dispatch.public_id %}" onsubmit="return confirm('هر {{ group_open_count }} تیکت باز این ارسال گروهی حلشده میشوند. آیا مطمئن هستید؟')">{% csrf_token %}<input type="hidden" name="return_ticket" value="{{ ticket.public_id }}"><button type="submit">✓ حلشدن تمامی تیکتهای گروه ({{ group_open_count }})</button></form>{% endif %}</div>
|
||||
<button type="button" class="ticket-dispatch-context-close" data-dismiss-dispatch-context aria-label="بستن پیام ارسال گروهی" title="بستن">×</button>
|
||||
</section>
|
||||
{% endif %}
|
||||
<div class="workspace-grid staff-conversation-grid">
|
||||
<aside class="workspace-side actions-side" id="ticket-actions" data-workspace-panel>
|
||||
<button type="button" class="panel-close" data-close-panel>×</button>
|
||||
|
||||
@@ -16,9 +16,12 @@
|
||||
<div class="dispatch-head-actions">
|
||||
<a class="button" href="{% url 'staff_tickets:dispatch_create' %}">+ ارسال گروهی جدید</a>
|
||||
{% if metrics.failed %}<form method="post" action="{% url 'staff_tickets:dispatch_retry' dispatch.public_id %}">{% csrf_token %}<button class="button button-danger-soft" type="submit">↻ تلاش مجدد {{ metrics.failed }} پیامک</button></form>{% endif %}
|
||||
{% if can_resolve_dispatch and open_ticket_count %}<form method="post" action="{% url 'staff_tickets:dispatch_resolve' dispatch.public_id %}" onsubmit="return confirm('با تأیید این عملیات، هر {{ open_ticket_count }} تیکت باز این ارسال گروهی یکجا حلشده میشوند. آیا ادامه میدهید؟')">{% csrf_token %}<button class="button dispatch-resolve-button" type="submit">✓ حلشدن همه تیکتها ({{ open_ticket_count }})</button></form>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if can_resolve_dispatch %}<aside class="dispatch-resolution-warning" role="note"><span aria-hidden="true">!</span><div><b>{% if open_ticket_count %}پایان رسیدگی گروهی{% else %}رسیدگی این گروه پایان یافته است{% endif %}</b><p>{% if open_ticket_count %}دکمه «حلشدن همه تیکتها» وضعیت {{ open_ticket_count }} تیکت باز این عملیات را همزمان تغییر میدهد. پاسخ تازه مشتری میتواند تیکت مربوط به خودش را دوباره باز کند.{% else %}همه تیکتهای این عملیات اکنون حل یا بسته هستند.{% endif %}</p></div></aside>{% endif %}
|
||||
|
||||
<section class="dispatch-original-message" aria-labelledby="dispatch-message-title">
|
||||
<header><div><span class="panel-index">متن اولیه کارشناس</span><h2 id="dispatch-message-title">پیام اصلی این ارسال</h2></div><span>شناسه عملیات: <b dir="ltr">{{ dispatch.public_id }}</b></span></header>
|
||||
<blockquote>{{ dispatch.body|linebreaksbr }}</blockquote>
|
||||
|
||||
@@ -178,3 +178,24 @@ def test_dispatch_report_presents_delivery_and_engagement_professionally(live_se
|
||||
page.set_viewport_size({"width":390,"height":844})
|
||||
expect(page.locator(".dispatch-detail-table tbody tr").first).to_be_visible()
|
||||
assert page.evaluate("document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1")
|
||||
|
||||
page.goto(f"{live_server.url}{reverse('staff_tickets:detail',args=[ticket.public_id])}", wait_until="domcontentloaded")
|
||||
expect(page.locator(".ticket-dispatch-context")).to_contain_text("استعلام حرفهای")
|
||||
dismiss_context = page.get_by_role("button", name="بستن پیام ارسال گروهی")
|
||||
expect(dismiss_context).to_be_visible()
|
||||
launch = page.locator(".composer-launch")
|
||||
expect(launch).to_be_visible()
|
||||
box = launch.bounding_box()
|
||||
assert box is not None
|
||||
assert box["x"] >= 0 and box["x"] + box["width"] <= 390
|
||||
assert box["y"] >= 0 and box["y"] + box["height"] <= 844
|
||||
assert page.evaluate("document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1")
|
||||
launch.click()
|
||||
close_composer = page.get_by_role("button", name="بستن", exact=True)
|
||||
expect(close_composer).to_be_visible()
|
||||
close_box = close_composer.bounding_box()
|
||||
assert close_box is not None
|
||||
assert close_box["x"] >= 0 and close_box["x"] + close_box["width"] <= 390
|
||||
assert close_box["y"] >= 0 and close_box["y"] + close_box["height"] <= 844
|
||||
dismiss_context.click()
|
||||
expect(page.locator(".ticket-dispatch-context")).to_have_count(0)
|
||||
@@ -49,6 +49,23 @@ def test_developer_credit_html_requires_explicit_trusted_html_switch(client):
|
||||
assert '<a href="https://example.com" style="color:red"><span>★</span> توسعه نمونه</a>' in rendered
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_customer_new_renders_developer_credit_once_directly_below_wizard(client):
|
||||
customer = User.objects.create_user(
|
||||
username="wizard-credit-customer", mobile="09120000404", account_type=User.AccountType.CUSTOMER
|
||||
)
|
||||
branding = SiteBranding.load()
|
||||
branding.developer_credit_text = "برنامه نویسی و توسعه: محمد جواد فصاحت | 09202202129"
|
||||
branding.save()
|
||||
client.force_login(customer)
|
||||
|
||||
content = client.get(reverse("tickets:new")).content.decode()
|
||||
assert content.count(branding.developer_credit_text) == 1
|
||||
assert '<footer class="ticket-wizard-credit">' in content
|
||||
assert content.index('</form>') < content.index('<footer class="ticket-wizard-credit">')
|
||||
assert '<footer class="site-footer">' not in content
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_staff_sidebar_brand_is_database_backed(client):
|
||||
user = User.objects.create_superuser(username="brand-admin", password="safe-password", account_type=User.AccountType.STAFF)
|
||||
|
||||
+89
-1
@@ -140,10 +140,11 @@ def test_unit_notification_destinations_normalize_and_reject_duplicate_superviso
|
||||
slug="notification-destinations",
|
||||
supervisor_mobile_1="0912 111 2233",
|
||||
supervisor_mobile_2="0935 222 3344",
|
||||
supervisor_mobile_4="0990 444 5566",
|
||||
)
|
||||
unit.full_clean()
|
||||
unit.save()
|
||||
assert unit.supervisor_mobiles == ("+989121112233", "+989352223344")
|
||||
assert unit.supervisor_mobiles == ("+989121112233", "+989352223344", "+989904445566")
|
||||
|
||||
unit.supervisor_mobile_3 = "09121112233"
|
||||
with pytest.raises(ValidationError, match="قبلاً"):
|
||||
@@ -158,6 +159,7 @@ def test_new_ticket_sms_is_queued_for_each_unit_supervisor_only_on_creation(cate
|
||||
unit.supervisor_mobile_1 = "09121112233"
|
||||
unit.supervisor_mobile_2 = "09352223344"
|
||||
unit.supervisor_mobile_3 = "09903334455"
|
||||
unit.supervisor_mobile_4 = "09214445566"
|
||||
unit.save()
|
||||
ticket = create_ticket(customer=customer, category=category, subject=" پیگیری بارگیری ", body="متن")
|
||||
jobs = list(NotificationJob.objects.filter(ticket=ticket, event_type="UNIT_SUPERVISOR_NEW_TICKET").order_by("id"))
|
||||
@@ -180,12 +182,98 @@ def test_new_ticket_sms_is_queued_for_each_unit_supervisor_only_on_creation(cate
|
||||
assert customer.local_mobile in message
|
||||
assert "موضوع: پیگیری بارگیری" in message
|
||||
assert f"دسته: {category.title}" in message
|
||||
assert "ارجاع: صف عمومی" in message
|
||||
|
||||
supervisor_job_count = NotificationJob.objects.filter(event_type="UNIT_SUPERVISOR_NEW_TICKET").count()
|
||||
reply_ticket(ticket_id=ticket.pk, user=customer, body="پاسخ جدید")
|
||||
assert NotificationJob.objects.filter(event_type="UNIT_SUPERVISOR_NEW_TICKET").count() == supervisor_job_count
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_unit_supervisor_sms_uses_editable_unit_template_and_selected_expert(category, customer, monkeypatch):
|
||||
from apps.notifications.management.commands.process_notifications import _send_sms
|
||||
from apps.notifications.models import SMSTemplate
|
||||
from apps.customers.models import CustomerProfile
|
||||
|
||||
unit = category.unit
|
||||
unit.supervisor_mobile_1 = "09125556677"
|
||||
unit.save()
|
||||
expert = make_staff("selected-supervisor-sms-expert")
|
||||
expert.staff_profile.display_name = "کارشناس انتخابی پیامک"
|
||||
expert.staff_profile.primary_unit = unit
|
||||
expert.staff_profile.save()
|
||||
CustomerProfile.objects.create(user=customer, first_name="سارا", last_name="احمدی")
|
||||
template = SMSTemplate.objects.create(
|
||||
unit=unit,
|
||||
event_type=SMSTemplate.Event.UNIT_SUPERVISOR_NEW_TICKET,
|
||||
title="الگوی سفارشی سرپرستان",
|
||||
body="{first_name} {last_name} | {unit_name} | {ticket_number} | {customer_mobile} | {category_name} | {expert_name}",
|
||||
)
|
||||
template.full_clean()
|
||||
ticket = create_ticket(
|
||||
customer=customer, category=category, subject="تست الگوی قابل تنظیم",
|
||||
selected_expert=expert, body="متن",
|
||||
)
|
||||
job = NotificationJob.objects.get(ticket=ticket, event_type=SMSTemplate.Event.UNIT_SUPERVISOR_NEW_TICKET)
|
||||
sent = []
|
||||
|
||||
class Provider:
|
||||
def send_message(self, mobile, message):
|
||||
sent.append((mobile, message))
|
||||
return type("Result", (), {"success": True})()
|
||||
|
||||
monkeypatch.setattr("apps.notifications.management.commands.process_notifications.get_sms_provider", lambda: Provider())
|
||||
_send_sms(job)
|
||||
assert sent == [(
|
||||
"+989125556677",
|
||||
f"سارا احمدی | {unit.name} | {ticket.ticket_number} | {customer.local_mobile} | {category.title} | کارشناس انتخابی پیامک",
|
||||
)]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_unit_supervisor_sms_handles_missing_customer_names_and_general_queue(category, customer, monkeypatch):
|
||||
from apps.notifications.management.commands.process_notifications import _send_sms
|
||||
from apps.notifications.models import SMSTemplate
|
||||
|
||||
category.unit.supervisor_mobile_1 = "09125556677"
|
||||
category.unit.save()
|
||||
SMSTemplate.objects.create(
|
||||
unit=category.unit,
|
||||
event_type=SMSTemplate.Event.UNIT_SUPERVISOR_NEW_TICKET,
|
||||
title="الگوی نام خالی سرپرستان",
|
||||
body="نام: {first_name} {last_name}\nنام جایگزین: {recipient_name}\nارجاع: {expert_name}",
|
||||
)
|
||||
ticket = create_ticket(customer=customer, category=category, subject="مشتری بدون نام", body="متن")
|
||||
job = NotificationJob.objects.get(ticket=ticket, event_type=SMSTemplate.Event.UNIT_SUPERVISOR_NEW_TICKET)
|
||||
sent = []
|
||||
|
||||
class Provider:
|
||||
def send_message(self, mobile, message):
|
||||
sent.append((mobile, message))
|
||||
return type("Result", (), {"success": True})()
|
||||
|
||||
monkeypatch.setattr("apps.notifications.management.commands.process_notifications.get_sms_provider", lambda: Provider())
|
||||
_send_sms(job)
|
||||
|
||||
assert sent == [(
|
||||
"+989125556677",
|
||||
"نام: \nنام جایگزین: ثبت نشده\nارجاع: صف عمومی",
|
||||
)]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_global_supervisor_sms_template_is_seeded_and_editable():
|
||||
from apps.notifications.models import SMSTemplate
|
||||
|
||||
template = SMSTemplate.objects.get(
|
||||
unit__isnull=True, event_type=SMSTemplate.Event.UNIT_SUPERVISOR_NEW_TICKET
|
||||
)
|
||||
assert "{expert_name}" in template.body
|
||||
assert "{customer_mobile}" in template.body
|
||||
assert "{category_name}" in template.body
|
||||
assert {"expert_name", "customer_mobile", "category_name"}.issubset(template.allowed_tags)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_staff_created_ticket_also_notifies_owning_unit_supervisors(category, customer):
|
||||
category.unit.supervisor_mobile_1 = "09124445566"
|
||||
|
||||
@@ -12,12 +12,83 @@ from apps.attachments.models import AttachmentPolicy
|
||||
from apps.customers.models import ContactGroup, CustomerProfile
|
||||
from apps.notifications.models import NotificationJob
|
||||
from apps.staff.models import OrganizationalUnit, StaffProfile
|
||||
from apps.tickets.models import GroupDispatch, Ticket, TicketCategory, TicketTag
|
||||
from apps.tickets.models import GroupDispatch, Ticket, TicketCategory, TicketEvent, TicketTag
|
||||
from apps.tickets.policies import TicketPolicy
|
||||
from apps.tickets.selectors import staff_ticket_rows
|
||||
from apps.tickets.services import create_ticket, dispatch_group_tickets, refresh_group_dispatch_status
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_dispatch_owner_can_resolve_all_open_group_tickets_from_report_or_ticket(client):
|
||||
unit = OrganizationalUnit.objects.create(name="حل گروهی", slug="resolve-dispatch")
|
||||
unit.permissions.add(*Permission.objects.filter(
|
||||
content_type__app_label="tickets",
|
||||
codename__in=("view_ticket", "dispatch_group_tickets", "change_ticket"),
|
||||
))
|
||||
staff = make_staff("dispatch-resolver", unit)
|
||||
first = make_contact("resolve-first", "09120000401")
|
||||
second = make_contact("resolve-second", "09120000402")
|
||||
group = ContactGroup.objects.create(name="گروه قابل حل", unit=unit)
|
||||
first.customer_profile.groups.add(group)
|
||||
second.customer_profile.groups.add(group)
|
||||
category = TicketCategory.objects.create(unit=unit, title="پیگیری گروه", slug="resolve-group")
|
||||
dispatch, tickets = dispatch_group_tickets(
|
||||
user=staff, unit=unit, contact_group=group, category=category,
|
||||
subject="پیگیری پاسخ گروه", body="لطفاً پاسخ دهید.",
|
||||
)
|
||||
tickets[1].status = Ticket.Status.CLOSED
|
||||
tickets[1].closed_at = timezone.now()
|
||||
tickets[1].save(update_fields=["status", "closed_at", "updated_at"])
|
||||
client.force_login(staff)
|
||||
|
||||
report = client.get(reverse("staff_tickets:dispatch_detail", args=[dispatch.public_id]))
|
||||
report_text = report.content.decode()
|
||||
assert report.context["open_ticket_count"] == 1
|
||||
assert "حلشدن همه تیکتها (1)" in report_text
|
||||
ticket_page = client.get(reverse("staff_tickets:detail", args=[tickets[0].public_id]))
|
||||
assert "وابسته به ارسال گروهی" in ticket_page.content.decode()
|
||||
assert dispatch.subject in ticket_page.content.decode()
|
||||
assert "حلشدن تمامی تیکتهای گروه (1)" in ticket_page.content.decode()
|
||||
|
||||
response = client.post(
|
||||
reverse("staff_tickets:dispatch_resolve", args=[dispatch.public_id]),
|
||||
{"return_ticket": tickets[0].public_id},
|
||||
)
|
||||
assert response.status_code == 302
|
||||
assert response.url == reverse("staff_tickets:detail", args=[tickets[0].public_id])
|
||||
tickets[0].refresh_from_db()
|
||||
tickets[1].refresh_from_db()
|
||||
assert tickets[0].status == Ticket.Status.RESOLVED
|
||||
assert tickets[0].resolved_at is not None
|
||||
assert tickets[1].status == Ticket.Status.CLOSED
|
||||
assert TicketEvent.objects.filter(
|
||||
ticket=tickets[0], event_type=TicketEvent.Type.STATUS_CHANGED,
|
||||
metadata__group_dispatch_id=dispatch.pk,
|
||||
).exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_dispatch_bulk_resolve_requires_change_ticket_permission(client):
|
||||
unit = OrganizationalUnit.objects.create(name="گروه فقط مشاهده", slug="read-only-dispatch")
|
||||
unit.permissions.add(*Permission.objects.filter(
|
||||
content_type__app_label="tickets", codename__in=("view_ticket", "dispatch_group_tickets")
|
||||
))
|
||||
staff = make_staff("read-only-dispatcher", unit)
|
||||
customer = make_contact("read-only-recipient", "09120000403")
|
||||
group = ContactGroup.objects.create(name="گروه فقط خواندنی", unit=unit)
|
||||
customer.customer_profile.groups.add(group)
|
||||
category = TicketCategory.objects.create(unit=unit, title="فقط مشاهده", slug="read-only-group")
|
||||
dispatch, tickets = dispatch_group_tickets(
|
||||
user=staff, unit=unit, contact_group=group, category=category,
|
||||
subject="عملیات فقط مشاهده", body="متن عملیات",
|
||||
)
|
||||
client.force_login(staff)
|
||||
assert client.get(reverse("staff_tickets:dispatch_detail", args=[dispatch.public_id])).context["can_resolve_dispatch"] is False
|
||||
client.post(reverse("staff_tickets:dispatch_resolve", args=[dispatch.public_id]))
|
||||
tickets[0].refresh_from_db()
|
||||
assert tickets[0].status != Ticket.Status.RESOLVED
|
||||
|
||||
|
||||
def make_staff(username, unit):
|
||||
user = User.objects.create_user(username=username, account_type=User.AccountType.STAFF)
|
||||
StaffProfile.objects.create(user=user, display_name=username, primary_unit=unit)
|
||||
|
||||
Reference in new issue
Block a user