Compare commits
585 Commits
Author | SHA1 | Date |
---|---|---|
|
cf47dd44a8 | 4 days ago |
|
40ca0bdf8d | 4 days ago |
|
87d1e0d3ae | 4 days ago |
|
5694081269 | 4 days ago |
|
2b01fc01ea | 6 days ago |
|
1e3e9b9942 | 6 days ago |
|
534dd60140 | 6 days ago |
|
0a6a630441 | 6 days ago |
|
7e135f349e | 2 weeks ago |
|
894f6e8173 | 3 weeks ago |
|
b845280151 | 1 month ago |
|
4174f33f08 | 1 month ago |
|
093fb9833a | 2 months ago |
|
eda2b2ac2d | 2 months ago |
|
7acc8a91d7 | 2 months ago |
|
062b6e21ee | 2 months ago |
|
f79b4d4caa | 2 months ago |
|
660bec8594 | 2 months ago |
|
d803964131 | 3 months ago |
|
80f9bb1b48 | 3 months ago |
|
f2f8f0216e | 5 months ago |
|
1fd49cd587 | 5 months ago |
|
102774dd64 | 5 months ago |
|
7d4b4d3546 | 6 months ago |
|
0602962a83 | 6 months ago |
|
115f913c7f | 7 months ago |
|
813258881f | 7 months ago |
|
9adf82de12 | 7 months ago |
|
e963322eb0 | 7 months ago |
|
f2b344cae9 | 7 months ago |
|
581d9d1b20 | 7 months ago |
|
563a744629 | 7 months ago |
|
b7f3f2ae14 | 7 months ago |
|
09755c3665 | 7 months ago |
|
0bf5d43599 | 7 months ago |
|
a35b7cf037 | 7 months ago |
|
48acdad5b7 | 7 months ago |
|
87e7c0acc4 | 7 months ago |
|
7b15478640 | 7 months ago |
|
1f85a341f6 | 7 months ago |
|
78834f8f11 | 7 months ago |
|
df1eae2b86 | 8 months ago |
|
fc494b57fc | 8 months ago |
|
b2df11beb3 | 8 months ago |
|
a2ce2534b6 | 8 months ago |
|
be3aea599b | 8 months ago |
|
4b2669227e | 8 months ago |
|
503547de78 | 8 months ago |
|
202ba82042 | 8 months ago |
|
be0b3d2766 | 8 months ago |
|
0f50b5b482 | 8 months ago |
|
2f7e8a9f0f | 8 months ago |
|
9a80374e24 | 8 months ago |
|
aee8f94b40 | 9 months ago |
|
2d3d23e10f | 9 months ago |
|
e4093fcce7 | 9 months ago |
|
e462485057 | 9 months ago |
|
46235a43ac | 9 months ago |
|
575e792481 | 9 months ago |
|
8330244008 | 9 months ago |
|
a7b0c870de | 9 months ago |
|
aa3b055276 | 9 months ago |
|
4baf742268 | 9 months ago |
|
b82c7ef7cb | 9 months ago |
|
43fc1e5e2d | 9 months ago |
|
324d8c0674 | 9 months ago |
|
9c3cdd6ea6 | 9 months ago |
|
001ce6740d | 9 months ago |
|
c29c970c4e | 9 months ago |
|
efd576f487 | 9 months ago |
|
65cfb289a2 | 9 months ago |
|
7917786e9b | 9 months ago |
|
1dd04972c6 | 9 months ago |
|
09d8d99059 | 9 months ago |
|
cbd7bbc8e1 | 9 months ago |
|
e5a0342d7b | 9 months ago |
|
7296ed954c | 9 months ago |
|
0de97b9ea8 | 9 months ago |
|
7ccc775791 | 9 months ago |
|
9d789723a8 | 9 months ago |
|
bb4eca48f6 | 9 months ago |
|
fb48ac668f | 9 months ago |
|
171a5f94c4 | 9 months ago |
|
59427a1a3a | 9 months ago |
|
ef8046d1ae | 9 months ago |
|
024f066075 | 9 months ago |
|
f37e5302a1 | 9 months ago |
|
c28d68960e | 9 months ago |
|
0ad7f3a470 | 10 months ago |
|
eb410adeb5 | 10 months ago |
|
c88adc4b5e | 10 months ago |
|
ad81a60d55 | 10 months ago |
|
5aa4d1766d | 10 months ago |
|
a7954e1114 | 10 months ago |
|
bde0e82ece | 10 months ago |
|
ec2e499abf | 10 months ago |
|
0af62c2e39 | 10 months ago |
|
8c16e0503e | 10 months ago |
|
7c3bfdfefe | 10 months ago |
|
6b396633cb | 10 months ago |
|
b439abba39 | 10 months ago |
|
1407d05f43 | 10 months ago |
|
c6283f0cc2 | 10 months ago |
|
e66597d4fa | 10 months ago |
|
5d56479f35 | 10 months ago |
|
df1a1f43bf | 10 months ago |
|
3bf491d8ad | 10 months ago |
|
f182ded513 | 10 months ago |
|
78793d3ddf | 10 months ago |
|
4dca7ee290 | 10 months ago |
|
0cc9a0bb17 | 10 months ago |
|
596dcbd5b0 | 10 months ago |
|
6cb61a850a | 10 months ago |
|
2e535eedfb | 10 months ago |
|
903efaead2 | 10 months ago |
|
65f2b996aa | 10 months ago |
|
592d396d78 | 10 months ago |
|
741e2e17d4 | 10 months ago |
|
fc78bd3e58 | 10 months ago |
|
ab82ef8d17 | 10 months ago |
|
9054538ce3 | 10 months ago |
|
d0af7acae3 | 10 months ago |
|
9ebab45035 | 10 months ago |
|
0dd8cc7a69 | 10 months ago |
|
7593637786 | 10 months ago |
|
945a1a7a17 | 10 months ago |
|
7d4b7bbba1 | 10 months ago |
|
c5343dd4b2 | 10 months ago |
|
4671179614 | 10 months ago |
|
ff8c072e07 | 10 months ago |
|
d33cbc625b | 10 months ago |
|
4c1855d65b | 10 months ago |
|
5a4a3f0214 | 10 months ago |
|
93cf0e5c5e | 10 months ago |
|
0bb1f24977 | 10 months ago |
|
4951196018 | 10 months ago |
|
3a63b11b3d | 10 months ago |
|
aba7c69a86 | 10 months ago |
|
e3f8262cff | 10 months ago |
|
fe58f01f22 | 10 months ago |
|
7045af0881 | 10 months ago |
|
1f574acab3 | 10 months ago |
|
df64cc306a | 10 months ago |
|
866433419c | 10 months ago |
|
577b033005 | 10 months ago |
|
c4d0ac4e4e | 10 months ago |
|
f0f7dc3939 | 10 months ago |
|
3839e2a212 | 10 months ago |
|
fd86659634 | 10 months ago |
|
c4b7fd4dde | 10 months ago |
|
cb0f519adf | 10 months ago |
|
6ecb8908f9 | 10 months ago |
|
4e5f880b4c | 10 months ago |
|
3e5528eeb6 | 10 months ago |
|
418d16c138 | 10 months ago |
|
8fff3a2885 | 10 months ago |
|
9a387067f9 | 10 months ago |
|
59c0f5df4f | 10 months ago |
|
2c674d5a03 | 10 months ago |
|
dcc20afd03 | 10 months ago |
|
07b9a7ddf5 | 10 months ago |
|
0f518d04cf | 10 months ago |
|
7719b8df3b | 10 months ago |
|
d26a0a94bd | 10 months ago |
|
2762b5237d | 10 months ago |
|
04287383dc | 10 months ago |
|
f34e2bc632 | 10 months ago |
|
ecdba1ee09 | 10 months ago |
|
663dbfcc50 | 10 months ago |
|
b285526dcb | 10 months ago |
|
03173157c7 | 11 months ago |
|
40053efbe8 | 11 months ago |
|
5aec058060 | 11 months ago |
|
e66431806b | 11 months ago |
|
d5777eaff1 | 11 months ago |
|
be31765e44 | 11 months ago |
|
bd5269f3f7 | 11 months ago |
|
31720a2930 | 11 months ago |
|
0c49bdcf6c | 11 months ago |
|
2f7184d837 | 11 months ago |
|
910c6c02af | 11 months ago |
|
20bd547fd6 | 11 months ago |
|
6e9faa39f1 | 11 months ago |
|
d65c12cb2c | 11 months ago |
|
f33dfa2415 | 11 months ago |
|
6ecdb54938 | 11 months ago |
|
b861cc66ae | 11 months ago |
|
eb4592acd0 | 11 months ago |
|
04552f3f00 | 11 months ago |
|
7b25c9962f | 11 months ago |
|
c101d6660c | 11 months ago |
|
722851aef6 | 11 months ago |
|
688c9caaee | 11 months ago |
|
a78f128c26 | 11 months ago |
|
c8299b747b | 11 months ago |
|
19fc024967 | 11 months ago |
|
be392588db | 11 months ago |
|
96ae28e947 | 11 months ago |
|
058e8ef33a | 11 months ago |
|
d1977c180e | 11 months ago |
|
ea3c468feb | 11 months ago |
|
12ab80403f | 11 months ago |
|
3fb6872538 | 11 months ago |
|
f315b22b90 | 11 months ago |
|
fa9feea260 | 11 months ago |
|
97013bf61f | 11 months ago |
|
acaf5a3de7 | 11 months ago |
|
4829b0c12c | 11 months ago |
|
7267eb145f | 11 months ago |
|
31abdfefea | 11 months ago |
|
d83f39efe6 | 11 months ago |
|
301031884b | 11 months ago |
|
53407fa57a | 11 months ago |
|
ef8cda5002 | 11 months ago |
|
2aa7769709 | 11 months ago |
|
d8cf4da101 | 11 months ago |
|
fdddfe8db6 | 11 months ago |
|
307b23021d | 11 months ago |
|
bdb3caf735 | 11 months ago |
|
628a9ab14f | 11 months ago |
|
6b470fedb8 | 11 months ago |
|
c475d7c766 | 11 months ago |
|
247caad4f5 | 11 months ago |
|
872f302ed4 | 11 months ago |
|
cb0c2a9e8e | 11 months ago |
|
4688c78dbd | 11 months ago |
|
eba209ea15 | 11 months ago |
|
fe562c260f | 11 months ago |
|
0932742837 | 11 months ago |
|
0f6cf8e1bc | 11 months ago |
|
8dbedbcf38 | 11 months ago |
|
4bc7e74825 | 11 months ago |
|
8f43258d99 | 11 months ago |
|
b1ddfc9bda | 11 months ago |
|
41b32aaff4 | 11 months ago |
|
8970be0c10 | 11 months ago |
|
7fb85e308d | 11 months ago |
|
0f65540733 | 11 months ago |
|
60be7b7457 | 11 months ago |
|
9b0427bc00 | 11 months ago |
|
65e534bf3e | 11 months ago |
|
7c85e61f78 | 11 months ago |
|
0601729c92 | 11 months ago |
|
63561e3804 | 11 months ago |
|
2254a09c5f | 11 months ago |
|
a186142cc0 | 11 months ago |
|
ceb3690530 | 11 months ago |
|
f308b994e3 | 11 months ago |
|
605eae0db5 | 11 months ago |
|
09cacf5f85 | 11 months ago |
|
b3faf7453e | 11 months ago |
|
e6af8c1e1e | 11 months ago |
|
98329e9876 | 11 months ago |
|
c03a8fd1fe | 11 months ago |
|
aae952f5e3 | 11 months ago |
|
86d0d175d3 | 11 months ago |
|
3040ca6220 | 11 months ago |
|
88b91387e4 | 11 months ago |
|
8e6db3bf85 | 11 months ago |
|
5b363c409f | 11 months ago |
|
6dce885450 | 11 months ago |
|
3ec112e88b | 11 months ago |
|
1cb54dbb75 | 11 months ago |
|
8dfb787db5 | 11 months ago |
|
f6b0dd8d04 | 11 months ago |
|
b2dcec8c37 | 11 months ago |
|
4ccb3cbdfd | 11 months ago |
|
59aa37cd99 | 11 months ago |
|
5e0013f0b4 | 11 months ago |
|
2004aafa6d | 11 months ago |
|
16a40e6280 | 11 months ago |
|
19dfc575b4 | 11 months ago |
|
e7a83df8a2 | 11 months ago |
|
c2e4b027a1 | 11 months ago |
|
4fbcc93134 | 11 months ago |
|
2692579437 | 11 months ago |
|
0647d845ba | 11 months ago |
|
b8b679a0ec | 11 months ago |
|
98937960e5 | 11 months ago |
|
80a54193a5 | 11 months ago |
|
b9a2b0e6ac | 11 months ago |
|
7f9025db5c | 11 months ago |
|
2caddfd092 | 11 months ago |
|
09d9cf5ec6 | 11 months ago |
|
e0784eb593 | 11 months ago |
|
5f4f525f45 | 11 months ago |
|
b6e57cd4ba | 11 months ago |
|
0d9a80cefd | 11 months ago |
|
ab527e36c2 | 11 months ago |
|
a63887a7b6 | 11 months ago |
|
2a193f9955 | 11 months ago |
|
26d354e0ea | 11 months ago |
|
2e68c7621d | 11 months ago |
|
6f761a233e | 11 months ago |
|
1dfc2d23d7 | 11 months ago |
|
2b9a365c45 | 11 months ago |
|
cae1a8774a | 11 months ago |
|
f68370fa72 | 11 months ago |
|
606c54a4f9 | 11 months ago |
|
655114ab44 | 11 months ago |
|
7b0a48360b | 11 months ago |
|
ab50a891f2 | 11 months ago |
|
01e4dbbf4f | 11 months ago |
|
af71ebf18e | 11 months ago |
|
71147a728d | 11 months ago |
|
e6bc9b2dc2 | 11 months ago |
|
58fc202f69 | 11 months ago |
|
3a592a640d | 11 months ago |
|
09091eaff5 | 11 months ago |
|
698a8b256c | 11 months ago |
|
3544419d11 | 11 months ago |
|
d9c45f9962 | 12 months ago |
|
83275f2d25 | 12 months ago |
|
9cb6d187c0 | 12 months ago |
|
05bbfaf49f | 12 months ago |
|
f1defd23e6 | 12 months ago |
|
fe868543ff | 12 months ago |
|
517d54614c | 12 months ago |
|
8a37c6f3f2 | 12 months ago |
|
ab01680747 | 12 months ago |
|
944c607c77 | 12 months ago |
|
0db9a5429a | 12 months ago |
|
2671015b43 | 12 months ago |
|
6cf4f81cdf | 12 months ago |
|
bc6b5cbed1 | 12 months ago |
|
0a391acdd0 | 12 months ago |
|
31d5db8387 | 12 months ago |
|
96c0e9f516 | 12 months ago |
|
2e49c01dac | 12 months ago |
|
d97638b843 | 12 months ago |
|
7a411ebfa9 | 12 months ago |
|
5b9f8eeb7f | 12 months ago |
|
dc87c7bdb6 | 12 months ago |
|
19ba6f3f2c | 12 months ago |
|
93572e6594 | 12 months ago |
|
2d9e577859 | 12 months ago |
|
d7118b0791 | 12 months ago |
|
cce861bd52 | 12 months ago |
|
b22e9d2edd | 12 months ago |
|
b5953c5c12 | 12 months ago |
|
9491ada91a | 12 months ago |
|
c6739e5279 | 12 months ago |
|
e1ec006772 | 12 months ago |
|
6ae1984b91 | 12 months ago |
|
5d78c9ee72 | 12 months ago |
|
d1ca39eccc | 12 months ago |
|
aac7effc0c | 12 months ago |
|
c6a868503c | 12 months ago |
|
e31c4903c4 | 12 months ago |
|
472273396c | 12 months ago |
|
2f2c86026a | 12 months ago |
|
ce921312af | 12 months ago |
|
cbfb2749d0 | 12 months ago |
|
db2ee69edf | 12 months ago |
|
a19d787fab | 12 months ago |
|
a0f81d26aa | 12 months ago |
|
852c784f9a | 12 months ago |
|
395e2e44db | 12 months ago |
|
2a20ed0f14 | 12 months ago |
|
b7d7c86319 | 12 months ago |
|
05a4106fd8 | 12 months ago |
|
5baeb5162f | 12 months ago |
|
67a21477c6 | 12 months ago |
|
4350f37a6a | 12 months ago |
|
70f5332839 | 1 year ago |
|
f77bcf4288 | 1 year ago |
|
f13241f262 | 1 year ago |
|
06db76ce86 | 1 year ago |
|
1a958639d5 | 1 year ago |
|
efa3f052a6 | 1 year ago |
|
efc12c25a9 | 1 year ago |
|
4fb6273ed1 | 1 year ago |
|
eca38ae611 | 1 year ago |
|
f11edab9cb | 1 year ago |
|
4bdf085a35 | 1 year ago |
|
8bfb9defdc | 1 year ago |
|
71c24be596 | 1 year ago |
|
7948d16613 | 1 year ago |
|
dfd7d2a749 | 1 year ago |
|
9008ae8511 | 1 year ago |
|
3f4fcc3789 | 1 year ago |
|
81397e7a68 | 1 year ago |
|
4940a54e13 | 1 year ago |
|
e3038e0a9d | 1 year ago |
|
7801719522 | 1 year ago |
|
551b082168 | 1 year ago |
|
3bbd694dc6 | 1 year ago |
|
ae5548a4d0 | 1 year ago |
|
c6e9857814 | 1 year ago |
|
e68e0ddd7d | 1 year ago |
|
52fcb20dbf | 1 year ago |
|
8b44f5ce5c | 1 year ago |
|
1b1db0937d | 1 year ago |
|
11b5a80986 | 1 year ago |
|
3b25a9dc7b | 1 year ago |
|
653533e80c | 1 year ago |
|
0eef2cee3a | 1 year ago |
|
e3727db78f | 1 year ago |
|
ed651ff3d4 | 1 year ago |
|
6caa17ea4c | 1 year ago |
|
8fd197a3e0 | 1 year ago |
|
27aeb82fb5 | 1 year ago |
|
42cb1d583f | 1 year ago |
|
e9026eb103 | 1 year ago |
|
acea97d3e5 | 1 year ago |
|
90f84fa874 | 1 year ago |
|
579689f3e0 | 1 year ago |
|
d01684b1c2 | 1 year ago |
|
80c1b98671 | 1 year ago |
|
2adeb31e73 | 1 year ago |
|
9304c4735d | 1 year ago |
|
e5407d6fa0 | 1 year ago |
|
40e8d97aa8 | 1 year ago |
|
64f690801e | 1 year ago |
|
cbf99e2718 | 1 year ago |
|
ce60237b3d | 1 year ago |
|
07b196fc66 | 1 year ago |
|
2ad323edbb | 1 year ago |
|
e3a28ebf25 | 1 year ago |
|
ff6071cd2f | 1 year ago |
|
cc2bf23bf5 | 1 year ago |
|
09726a51bf | 1 year ago |
|
f440b08fad | 1 year ago |
|
1534ce6979 | 1 year ago |
|
16a21aae43 | 1 year ago |
|
81f1c9cea9 | 1 year ago |
|
4b77aa689e | 1 year ago |
|
d8b141cda8 | 1 year ago |
|
fcef4c486b | 1 year ago |
|
bebb8c0a09 | 1 year ago |
|
461263839b | 1 year ago |
|
545bee21cb | 1 year ago |
|
d0a4453ce6 | 1 year ago |
|
6f446d852a | 1 year ago |
|
1084db4b96 | 1 year ago |
|
a3d02f4caf | 1 year ago |
|
51aa59abe1 | 1 year ago |
|
a5b82e245b | 1 year ago |
|
c843fab969 | 1 year ago |
|
37c3b5aa45 | 1 year ago |
|
51834fbc97 | 1 year ago |
|
cbdb737163 | 1 year ago |
|
65064937f0 | 1 year ago |
|
e4cc07eefe | 1 year ago |
|
056a006847 | 1 year ago |
|
2bdf22c140 | 1 year ago |
|
f2177a9e8e | 1 year ago |
|
752bd729eb | 1 year ago |
|
702d7876c0 | 1 year ago |
|
1b97cb41dd | 1 year ago |
|
ad10ef9313 | 1 year ago |
|
3c533c96b4 | 1 year ago |
|
040fdb9d3a | 1 year ago |
|
261849fa5f | 1 year ago |
|
7409ec1c16 | 1 year ago |
|
079b74c3b4 | 1 year ago |
|
b0d4943f09 | 1 year ago |
|
efad9d8697 | 1 year ago |
|
4afb1d4371 | 1 year ago |
|
697fa00be3 | 1 year ago |
|
e9289bbe80 | 1 year ago |
|
3ec9a43833 | 1 year ago |
|
86242addab | 1 year ago |
|
219372c06e | 1 year ago |
|
240697a42d | 1 year ago |
|
5c236defd1 | 1 year ago |
|
ce345c7347 | 1 year ago |
|
4987b7bcd8 | 1 year ago |
|
c4145a13df | 1 year ago |
|
bf1d668301 | 1 year ago |
|
cacc4832c5 | 1 year ago |
|
1194cc84a0 | 1 year ago |
|
e9b7aec521 | 1 year ago |
|
7b2c836e91 | 1 year ago |
|
a61ef8882b | 1 year ago |
|
a7c87170a0 | 1 year ago |
|
2a8dcabdec | 1 year ago |
|
e3dc13bc6c | 1 year ago |
|
f8ffaf899d | 1 year ago |
|
32b097c5b7 | 1 year ago |
|
d24c34d4ac | 1 year ago |
|
a579ee4ee5 | 1 year ago |
|
232a8f85c8 | 1 year ago |
|
2cb059cc06 | 1 year ago |
|
f323f0b511 | 1 year ago |
|
f22c9eb497 | 1 year ago |
|
74a68d705c | 1 year ago |
|
c352581f98 | 1 year ago |
|
fa0550dfdb | 1 year ago |
|
5d4b5cc8f1 | 1 year ago |
|
bfc994cd12 | 1 year ago |
|
aa501c7f3a | 1 year ago |
|
a7f3faac13 | 1 year ago |
|
1142a1a893 | 1 year ago |
|
849e6ceef0 | 1 year ago |
|
043c02f8a8 | 1 year ago |
|
749084f0aa | 1 year ago |
|
ca0edbd63a | 1 year ago |
|
30012ae37e | 1 year ago |
|
b62bba8587 | 1 year ago |
|
e2766501ca | 1 year ago |
|
bff5197765 | 1 year ago |
|
5913970154 | 1 year ago |
|
61076ed908 | 1 year ago |
|
97b19cf951 | 1 year ago |
|
680364eae3 | 1 year ago |
|
0c91e88674 | 1 year ago |
|
1e439621a5 | 1 year ago |
|
8e07c82614 | 1 year ago |
|
5412b872a0 | 1 year ago |
|
3465c7f575 | 1 year ago |
|
af2ce43b2a | 1 year ago |
|
8d3649da9d | 1 year ago |
|
63a32f5412 | 1 year ago |
|
9522fece90 | 1 year ago |
|
600ba44d8d | 1 year ago |
|
322646a9dc | 1 year ago |
|
e60be41b98 | 1 year ago |
|
428e8f4aa4 | 1 year ago |
|
a1890df323 | 1 year ago |
|
9fef49263b | 1 year ago |
|
28e2b9f6a6 | 1 year ago |
|
e31a20e92d | 1 year ago |
|
3235e7e574 | 1 year ago |
|
9071e87bc7 | 1 year ago |
|
83d08dd099 | 1 year ago |
|
b682fcb9cb | 1 year ago |
|
96c8017271 | 1 year ago |
|
733c1c0fec | 1 year ago |
|
2c8da99712 | 1 year ago |
|
30a8301329 | 1 year ago |
|
6b2d3d2b04 | 1 year ago |
|
877a020dc0 | 1 year ago |
|
5ef8c9a72f | 1 year ago |
|
d8bc4d2ff4 | 1 year ago |
|
b5c13f238d | 1 year ago |
|
bf8c45033b | 1 year ago |
|
5b30aec3e5 | 1 year ago |
|
6e66a31bb0 | 1 year ago |
|
844ad99bbd | 1 year ago |
|
a608c457b8 | 1 year ago |
|
6ad342cb2e | 1 year ago |
|
bc18a05254 | 1 year ago |
|
c7f29f4999 | 1 year ago |
|
65eaf11d42 | 1 year ago |
|
bf50e29ed7 | 1 year ago |
|
66c7ba9574 | 1 year ago |
|
b568379f41 | 1 year ago |
|
887b47a55a | 1 year ago |
|
d443f21805 | 1 year ago |
|
8d3a7354c7 | 1 year ago |
|
4fff1229fd | 1 year ago |
|
abf527a53d | 1 year ago |
|
3a6ce1d366 | 1 year ago |
|
ceb397a895 | 1 year ago |
|
0be4bc9504 | 1 year ago |
|
ea0bec8579 | 1 year ago |
|
045eec2566 | 1 year ago |
|
41a9c2a590 | 1 year ago |
|
c06df03754 | 1 year ago |
|
bff8a1ea78 | 1 year ago |
|
49f8cafade | 1 year ago |
|
384be3171b | 1 year ago |
|
7f3ed0b42c | 1 year ago |
|
696f2bb816 | 1 year ago |
|
64f32d9093 | 1 year ago |
|
03ac61fad0 | 1 year ago |
|
ae0324f8cf | 1 year ago |
|
df915c6114 | 1 year ago |
|
22aa244f9e | 1 year ago |
|
d3b53b766b | 1 year ago |
|
2aa20577d5 | 1 year ago |
|
75fa47cfe0 | 1 year ago |
|
200dbaad9e | 1 year ago |
|
fc81ca0363 | 1 year ago |
|
0d87b5dcb4 | 1 year ago |
|
6ba07440a7 | 1 year ago |
|
77a03b89bc | 1 year ago |
|
e3a1788b7a | 1 year ago |
|
458986f1ce | 1 year ago |
|
74d3b79019 | 1 year ago |
|
c616921548 | 1 year ago |
|
461e37c720 | 1 year ago |
|
353e713827 | 1 year ago |
|
7e0773fcb2 | 1 year ago |
@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:react/jsx-runtime', 'plugin:react-hooks/recommended'],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||
settings: { react: { version: '18.2' } },
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||
'no-unused-vars': ['warn', { args: 'after-used', vars: 'all' }],
|
||||
'react/prop-types': 'off',
|
||||
'react-hooks/rules-of-hooks': 'warn',
|
||||
},
|
||||
};
|
@ -0,0 +1 @@
|
||||
npm version patch
|
@ -0,0 +1 @@
|
||||
npm version prerelease
|
@ -0,0 +1,92 @@
|
||||
CREATE TABLE auth_role
|
||||
(
|
||||
[role_id] [int] IDENTITY(1,1) NOT NULL,
|
||||
[role_name] [nvarchar](255) NOT NULL,
|
||||
[created_on] [datetime] NOT NULL,
|
||||
CONSTRAINT [PK_auth_role] PRIMARY KEY CLUSTERED
|
||||
(
|
||||
[role_id] ASC
|
||||
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
|
||||
) ON [PRIMARY]
|
||||
|
||||
ALTER TABLE auth_role ADD CONSTRAINT [DF_auth_role_created_on] DEFAULT (getdate()) FOR [created_on]
|
||||
|
||||
CREATE TABLE auth_permission
|
||||
(
|
||||
[role_id] [int] NOT NULL,
|
||||
[res_id] [int] NOT NULL
|
||||
) ON [PRIMARY]
|
||||
|
||||
CREATE TABLE auth_resource
|
||||
(
|
||||
[res_id] [int] IDENTITY(1,1) NOT NULL,
|
||||
[res_name] [nvarchar](255) NOT NULL,
|
||||
[res_pattern] [nvarchar](255) NOT NULL,
|
||||
[res_category] [nvarchar](255) NOT NULL,
|
||||
CONSTRAINT [PK_auth_resource] PRIMARY KEY CLUSTERED
|
||||
(
|
||||
[res_id] ASC
|
||||
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
|
||||
) ON [PRIMARY]
|
||||
|
||||
INSERT INTO [dbo].[auth_role] ([role_name])
|
||||
VALUES ('系统管理员')
|
||||
INSERT INTO [dbo].[auth_role] ([role_name])
|
||||
VALUES ('国内供应商')
|
||||
INSERT INTO [dbo].[auth_role] ([role_name])
|
||||
VALUES ('海外供应商')
|
||||
INSERT INTO [dbo].[auth_role] ([role_name])
|
||||
VALUES ('客服组')
|
||||
INSERT INTO [dbo].[auth_role] ([role_name])
|
||||
VALUES ('产品组')
|
||||
INSERT INTO [dbo].[auth_role] ([role_name])
|
||||
VALUES ('技术研发部')
|
||||
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('所有权限', '*', 'system')
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('管理账号', '/account/management', 'system')
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('新增账号', '/account/new', 'system')
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('禁用账号', '/account/disable', 'system')
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('重置密码', '/account/reset-password', 'system')
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('管理角色', '/account/role-new', 'system')
|
||||
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('所有海外功能', '/oversea/all', 'oversea')
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('所有国内功能', '/domestic/all', 'domestic')
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('所有机票功能', '/air-ticket/all', 'air-ticket')
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('所有火车票功能', '/train-ticket/all', 'train-ticket')
|
||||
|
||||
-- 价格管理
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('管理产品', '/products/*', 'products')
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('新增产品', '/products/new', 'products')
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('审核信息', '/products/info/audit', 'products')
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('录入信息', '/products/info/put', 'products')
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('审核价格', '/products/offer/audit', 'products')
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('录入价格', '/products/offer/put', 'products')
|
||||
|
||||
-- 默认页面
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('最新计划', 'route=/reservation/newest', 'page')
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('机票订票', 'route=/airticket', 'page')
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('产品管理(客服)', 'route=/products', 'page')
|
||||
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
|
||||
VALUES ('产品管理(供应商)', 'route=/products/edit', 'page')
|
||||
|
||||
INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id])
|
||||
VALUES (1, 1)
|
Binary file not shown.
@ -1,28 +1,49 @@
|
||||
{
|
||||
"name": "global.highlights.hub",
|
||||
"name": "global-highlights-hub",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "2.0.20",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"4test": "vite build --mode test",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.5.1",
|
||||
"@react-pdf/renderer": "^3.4.0",
|
||||
"antd": "^5.4.2",
|
||||
"mobx": "^6.9.0",
|
||||
"mobx-react": "^7.6.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"antd": "^5.17.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"docx": "^8.5.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^23.11.5",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next-http-backend": "^2.5.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^14.1.2",
|
||||
"react-router-dom": "^6.10.0",
|
||||
"react-to-pdf": "^1.0.1"
|
||||
"react-to-pdf": "^1.0.1",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-legacy": "^4.0.2",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"vite": "^4.2.0"
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"vite": "^4.2.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-windicss": "^1.9.3",
|
||||
"windicss": "^3.5.6"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
{
|
||||
"CurrentPassword": "Current password",
|
||||
"NewPassword": "New password",
|
||||
"ReenterPassword": "Reenter password",
|
||||
"Validation": {
|
||||
"Success": "Your password has been successfully updated.",
|
||||
"Fail": "Failed to change password. Please try again.",
|
||||
"CurrentPassword": "Please input your password.",
|
||||
"NewPassword": "Please input your new password.",
|
||||
"ReenterPassword": "Please reenter your password.",
|
||||
"username": "请重复输入用户名。",
|
||||
"realname": "请重复输入真实姓名。",
|
||||
"email": "请重复输入邮箱。",
|
||||
"travelAgency": "请重复输供应商。",
|
||||
"role": "请重复输入角色。",
|
||||
"roleName": "请重复输入角色名称。"
|
||||
},
|
||||
"createdOn": "Created on",
|
||||
"action": "Action",
|
||||
"action.edit": "Edit",
|
||||
"action.enable": "Enable",
|
||||
"action.disable": "Disable",
|
||||
"action.enable.title": "Do you want to enable account?",
|
||||
"action.disable.title": "Do you want to disable account?",
|
||||
"action.resetPassword": "Reset Password",
|
||||
"action.resetPassword.tile": "Do you want to reset password?",
|
||||
|
||||
"accountList": "Account List",
|
||||
"newAccount": "New Account",
|
||||
"detail": "Detail",
|
||||
"username": "Username",
|
||||
"realname": "Realname",
|
||||
"travelAgency": "Travel Agency",
|
||||
"travelAgencyName": "Travel Agency Name",
|
||||
"email": "Email",
|
||||
"lastLogin": "Last Login",
|
||||
|
||||
"roleList": "Role List",
|
||||
"newRole": "New Role",
|
||||
"roleName": "Role Name",
|
||||
"permission": "Permission"
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
{
|
||||
"lang": {
|
||||
"en": "English",
|
||||
"zh": "中文"
|
||||
},
|
||||
"Search": "Search",
|
||||
"Reset": "Reset",
|
||||
"Cancel": "Cancel",
|
||||
"Submit": "Submit",
|
||||
"Confirm": "Confirm",
|
||||
"Close": "Close",
|
||||
"Save": "Save",
|
||||
"New": "New",
|
||||
"Edit": "Edit",
|
||||
"Audit": "Audit",
|
||||
"Delete": "Delete",
|
||||
"Add": "Add",
|
||||
"View": "View",
|
||||
"Back": "Back",
|
||||
"Download": "Download",
|
||||
"Upload": "Upload",
|
||||
"preview": "Preview",
|
||||
"Total": "Total",
|
||||
"Action": "Action",
|
||||
"Import": "Import",
|
||||
"Export": "Export",
|
||||
"Copy": "Copy",
|
||||
"sureCancel": "Are you sure to cancel?",
|
||||
"sureDelete": "Are you sure to delete?",
|
||||
"sureSubmit": "Are you sure to submit?",
|
||||
"Yes": "Yes",
|
||||
"No": "No",
|
||||
"Success": "Success",
|
||||
"Failed": "Failed",
|
||||
"All": "All",
|
||||
"Table": {
|
||||
"Total": "Total {{total}} items"
|
||||
},
|
||||
"Login": "Login",
|
||||
"Username": "Username",
|
||||
"Realname": "Realname",
|
||||
"Password": "Password",
|
||||
"ChangePassword": "Change password",
|
||||
"Profile": "Profile",
|
||||
"Logout": "Logout",
|
||||
"LoginTimeout": "Login timeout",
|
||||
"LoginTimeoutTip": "Please input your password",
|
||||
"userProfile": "User Profile",
|
||||
"Telephone": "Telephone",
|
||||
"Email": "Email address",
|
||||
"Address": "Address",
|
||||
"Company": "Company",
|
||||
"Department": "Department",
|
||||
"datetime": {
|
||||
"thisWeek": "This Week",
|
||||
"lastWeek": "Last Week",
|
||||
"thisMonth": "This Month",
|
||||
"lastMonth": "Last Month",
|
||||
"nextMonth": "Next Month",
|
||||
"lastThreeMonth": "Last Three Month",
|
||||
"nextThreeMonth": "Next Three Month",
|
||||
"firstHalfYear": "First Half Year",
|
||||
"latterHalfYear": "Latter Half Year",
|
||||
"thisYear": "This Year"
|
||||
},
|
||||
"weekdays": {
|
||||
"1": "Monday",
|
||||
"2": "Tuesday",
|
||||
"3": "Wednesday",
|
||||
"4": "Thursday",
|
||||
"5": "Friday",
|
||||
"6": "Saturday",
|
||||
"7": "Sunday"
|
||||
},
|
||||
"weekdaysShort": {
|
||||
"1": "Mon",
|
||||
"2": "Tue",
|
||||
"3": "Wed",
|
||||
"4": "Thu",
|
||||
"5": "Fri",
|
||||
"6": "Sat",
|
||||
"7": "Sun"
|
||||
},
|
||||
"menu": {
|
||||
"Reservation": "Reservation",
|
||||
"Invoice": "Invoice",
|
||||
"Feedback": "Feedback",
|
||||
"Notice": "Notice",
|
||||
"Report": "Report",
|
||||
"Airticket": "AirTicket",
|
||||
"Trainticket": "TrainTicket",
|
||||
"Products": "Products"
|
||||
},
|
||||
"Validation": {
|
||||
"Title": "Notification",
|
||||
"LoginFailed": "Incorrect password, Login failed.",
|
||||
"UsernameIsEmpty": "Please input your username",
|
||||
"PasswordIsEmpty": "Please input your password"
|
||||
},
|
||||
"invoiceStatus": {
|
||||
"Status": "Status",
|
||||
"Not_submitted": "Not submitted",
|
||||
"Submitted": "Submitted",
|
||||
"Travel_advisor_approved": "Travel advisor approved",
|
||||
"Finance_Dept_arrproved": "Finance Dept arrproved",
|
||||
"Paid": "Paid"
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
{
|
||||
"ArrivalDate": "Arrival Date",
|
||||
"RefNo": "Reference number",
|
||||
"unconfirmed": "Unconfirmed",
|
||||
"Pax": "Pax",
|
||||
"Status": "Status",
|
||||
"City": "City",
|
||||
"Guide": "Guide",
|
||||
"ResSendingDate": "Res. sending date",
|
||||
"3DGuideTip": "Reservations without the tour guide information will be highlighted in red if the arrival date is within 3 days.",
|
||||
"Attachments": "Attachments",
|
||||
"ConfirmationDate": "Confirmation Date",
|
||||
"ConfirmationDetails": "Confirmation Details",
|
||||
"PNR": "PASSAGER NAME RECORD",
|
||||
|
||||
"#": "#"
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"report": {
|
||||
"GetReport": "Get Report"
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
{
|
||||
"CurrentPassword": "当前密码",
|
||||
"NewPassword": "新密码",
|
||||
"ReenterPassword": "重复输入密码",
|
||||
"Validation": {
|
||||
"Success": "密码更新成功",
|
||||
"Fail": "密码更新失败",
|
||||
"CurrentPassword": "请输入密码。",
|
||||
"NewPassword": "请输入新密码。",
|
||||
"ReenterPassword": "请重复输入密码。",
|
||||
"username": "请重复输入用户名。",
|
||||
"realname": "请重复输入真实姓名。",
|
||||
"email": "请重复输入邮箱。",
|
||||
"travelAgency": "请重复输供应商。",
|
||||
"role": "请重复输入角色。",
|
||||
"roleName": "请重复输入角色名称。"
|
||||
},
|
||||
"createdOn": "创建时间",
|
||||
"action": "操作",
|
||||
"action.edit": "编辑",
|
||||
"action.enable": "启用",
|
||||
"action.disable": "禁用",
|
||||
"action.enable.title": "确定启用该账号吗?",
|
||||
"action.disable.title": "确定禁用该账号吗?",
|
||||
"action.resetPassword": "重置密码",
|
||||
"action.resetPassword.tile": "确定重置账号密码吗?",
|
||||
|
||||
"accountList": "管理账号",
|
||||
"newAccount": "新增账号",
|
||||
"detail": "详细信息",
|
||||
"username": "用户名",
|
||||
"realname": "姓名",
|
||||
"travelAgency": "供应商",
|
||||
"travelAgencyName": "供应商名称",
|
||||
"email": "邮箱地址",
|
||||
"lastLogin": "最后登陆时间",
|
||||
|
||||
"roleList": "管理角色",
|
||||
"newRole": "新增角色",
|
||||
"roleName": "角色名称",
|
||||
"permission": "权限"
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
{
|
||||
"lang": {
|
||||
"en": "English",
|
||||
"zh": "中文"
|
||||
},
|
||||
"Search": "查询",
|
||||
"Reset": "重置",
|
||||
"Cancel": "取消",
|
||||
"Submit": "提交",
|
||||
"Confirm": "确认",
|
||||
"Close": "关闭",
|
||||
"Save": "保存",
|
||||
"New": "新增",
|
||||
"Edit": "编辑",
|
||||
"Audit": "审核",
|
||||
"Delete": "删除",
|
||||
"Add": "添加",
|
||||
"View": "查看",
|
||||
"Back": "返回",
|
||||
"Download": "下载",
|
||||
"Upload": "上传",
|
||||
"preview": "预览",
|
||||
"Total": "总数",
|
||||
"Action": "操作",
|
||||
"Import": "导入",
|
||||
"Export": "导出",
|
||||
"Copy": "复制",
|
||||
"sureCancel": "确定取消?",
|
||||
"sureDelete": "确定删除?",
|
||||
"sureSubmit": "确定提交?",
|
||||
"Yes": "是",
|
||||
"No": "否",
|
||||
"Success": "成功",
|
||||
"Failed": "失败",
|
||||
"All": "所有",
|
||||
"Table": {
|
||||
"Total": "共 {{total}} 条"
|
||||
},
|
||||
"Login": "登录",
|
||||
"Username": "账号",
|
||||
"Realname": "姓名",
|
||||
"Password": "密码",
|
||||
"ChangePassword": "修改密码",
|
||||
"Profile": "账户中心",
|
||||
"Logout": "退出",
|
||||
"LoginTimeout": "登录超时",
|
||||
"LoginTimeoutTip": "请输入密码",
|
||||
"userProfile": "账号信息",
|
||||
"Telephone": "联系电话",
|
||||
"Email": "电子邮箱",
|
||||
"Address": "公司地址",
|
||||
"Company": "公司名称",
|
||||
"Department": "部门",
|
||||
"datetime": {
|
||||
"thisWeek": "本周",
|
||||
"lastWeek": "上周",
|
||||
"thisMonth": "本月",
|
||||
"lastMonth": "上月",
|
||||
"nextMonth": "下月",
|
||||
"lastThreeMonth": "前三个月",
|
||||
"nextThreeMonth": "后三个月",
|
||||
"firstHalfYear": "上半年",
|
||||
"latterHalfYear": "下半年",
|
||||
"thisYear": "今年"
|
||||
},
|
||||
"weekdays": {
|
||||
"1": "周一",
|
||||
"2": "周二",
|
||||
"3": "周三",
|
||||
"4": "周四",
|
||||
"5": "周五",
|
||||
"6": "周六",
|
||||
"7": "周日"
|
||||
},
|
||||
"weekdaysShort": {
|
||||
"1": "一",
|
||||
"2": "二",
|
||||
"3": "三",
|
||||
"4": "四",
|
||||
"5": "五",
|
||||
"6": "六",
|
||||
"7": "日"
|
||||
},
|
||||
"menu": {
|
||||
"Reservation": "团预订",
|
||||
"Invoice": "账单",
|
||||
"Feedback": "反馈表",
|
||||
"Notice": "通知",
|
||||
"Report": "质量评分",
|
||||
"Airticket": "机票订票",
|
||||
"Trainticket": "火车订票",
|
||||
"Products": "产品管理"
|
||||
},
|
||||
"Validation": {
|
||||
"Title": "温馨提示",
|
||||
"LoginFailed": "密码错误,登陆失败。",
|
||||
"UsernameIsEmpty": "请输入账号",
|
||||
"PasswordIsEmpty": "请输入密码"
|
||||
},
|
||||
"invoiceStatus": {
|
||||
"Status": "审核状态",
|
||||
"Not_submitted": "待提交",
|
||||
"Submitted": "待审核",
|
||||
"Travel_advisor_approved": "顾问已审核",
|
||||
"Finance_Dept_arrproved": "财务已审核",
|
||||
"Paid": "已打款"
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"ArrivalDate": "抵达日期",
|
||||
"RefNo": "团号",
|
||||
"unconfirmed": "未确认",
|
||||
"Pax": "人数",
|
||||
"Status": "状态",
|
||||
"City": "城市",
|
||||
"Guide": "导游",
|
||||
"ResSendingDate": "发送时间",
|
||||
"3DGuideTip": "红色突出显示:抵达日期在 3 天内,没有导游信息的预订。",
|
||||
"Attachments": "附件",
|
||||
"ConfirmationDate": "确认日期",
|
||||
"ConfirmationDetails": "确认信息",
|
||||
"PNR": "旅客订座记录",
|
||||
"#": "#"
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"report": {
|
||||
"GetReport": "获取报告"
|
||||
}
|
||||
}
|
@ -1,19 +1,6 @@
|
||||
.logo {
|
||||
float: left;
|
||||
height: 36px;
|
||||
margin: 16px 24px 16px 0;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.reservation-highlight {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
background-color: rgba(255, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
#error-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
.ant-table-wrapper.border-collapse table {
|
||||
border-collapse: collapse;
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { Select } from 'antd';
|
||||
import { useProductsAuditStates } from '@/hooks/useProductsSets';
|
||||
|
||||
const AuditStateSelector = ({ ...props }) => {
|
||||
const states = useProductsAuditStates();
|
||||
return (
|
||||
<>
|
||||
<Select labelInValue allowClear options={states} {...props}/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default AuditStateSelector;
|
@ -0,0 +1,16 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'antd';
|
||||
import { isNotEmpty } from '@/utils/commons';
|
||||
|
||||
const BackBtn = ({to, ...props}) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<>
|
||||
{isNotEmpty(to) ? <Link to={to} className='px-4'>{t('Back')}</Link> : <Button type='link' onClick={() => navigate(-1)}>{t('Back')}</Button>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default BackBtn;
|
@ -0,0 +1,29 @@
|
||||
import { createContext, useEffect, useState } from 'react';
|
||||
import {} from 'antd';
|
||||
import SearchInput from './SearchInput';
|
||||
import { fetchJSON } from '@/utils/request';
|
||||
import { HT_HOST } from '@/config';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
//供应商列表
|
||||
export const fetchCityList = async (q) => {
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/search_cities`, { q });
|
||||
return errcode !== 0 ? [] : result;
|
||||
};
|
||||
|
||||
const CitySelector = ({ ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<SearchInput
|
||||
placeholder={t('products:City')}
|
||||
mode={null}
|
||||
maxTagCount={0}
|
||||
{...props}
|
||||
fetchOptions={fetchCityList}
|
||||
map={{ city_name: 'label', city_id: 'value' }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default CitySelector;
|
@ -0,0 +1,94 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Tag, Button, message } from 'antd';
|
||||
import { CaretUpOutlined, CaretDownOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||
import { utils, writeFile } from "xlsx";
|
||||
import { isEmpty, getNestedValue } from "../utils/commons";
|
||||
|
||||
/**
|
||||
* @property diffPercent
|
||||
* @property diffData
|
||||
* @property data1
|
||||
* @property data2
|
||||
*/
|
||||
export const VSTag = (props) => {
|
||||
const { diffPercent, diffData, data1, data2 } = props;
|
||||
const CaretIcon = parseInt(diffPercent) < 0 ? CaretDownOutlined : CaretUpOutlined;
|
||||
const tagColor = parseInt(diffPercent) < 0 ? 'gold' : 'lime';
|
||||
return parseInt(diffPercent) === 0 ? (
|
||||
'-'
|
||||
) : (
|
||||
<span>
|
||||
{/* <div>
|
||||
{data1} vs {data2}
|
||||
</div> */}
|
||||
<Tag icon={<CaretIcon />} color={tagColor}>
|
||||
{diffPercent}<span>%</span>{' '}<span>{diffData}</span>
|
||||
</Tag>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出表格数据存为xlsx
|
||||
*/
|
||||
export const TableExportBtn = (props) => {
|
||||
const output_name = `${props.label}`;
|
||||
const [columnsMap, setColumnsMap] = useState([]);
|
||||
const [summaryRow, setSummaryRow] = useState({});
|
||||
useEffect(() => {
|
||||
const r1 = props.columns.reduce((r, v) => ({
|
||||
...r,
|
||||
...(v.children ? v.children.reduce((rc, vc, ci) => ({
|
||||
...rc,
|
||||
...(vc?.titleX ? {[`${v?.titleX || v.title},${vc.titleX}`]: vc.titleX } : {[(v?.titleX || v.title) + (ci || '')]: `${vc?.titleX || vc?.title || ''}`}),
|
||||
}), {}) : {})
|
||||
}), {});
|
||||
const flatCols = props.columns.flatMap((v, k) =>
|
||||
v.children ? v.children.map((vc, ci) => ({ ...vc, title: `${v?.titleX || v.title}` + (vc?.titleX ? `,${vc.titleX}` : (ci || '')) })) : {...v, title: `${v?.titleX || v.title}`}
|
||||
);
|
||||
// .filter((c) => c.dataIndex)
|
||||
// !['string', 'number'].includes(typeof vc.title) ? `${v?.titleX || v.title}` : `${v?.titleX || v.title}-${vc.title || ''}`
|
||||
;
|
||||
setColumnsMap(flatCols);
|
||||
// console.log('flatCols', flatCols);
|
||||
|
||||
setSummaryRow(r1);
|
||||
// console.log('summaryRow', r1);
|
||||
|
||||
return () => {};
|
||||
}, [props.columns]);
|
||||
|
||||
const onExport = () => {
|
||||
if (isEmpty(props.dataSource)) {
|
||||
message.warning('无结果.');
|
||||
return false;
|
||||
}
|
||||
const data = props.dataSource.map((item) => {
|
||||
const itemMapped = columnsMap.reduce((sv, kset) => {
|
||||
const render_val = typeof kset?.render === 'function' ? kset.render('', item) : null;
|
||||
const data_val = kset?.dataIndex ? (Array.isArray(kset.dataIndex) ? getNestedValue(item, kset.dataIndex) : item[kset.dataIndex]) : undefined;
|
||||
const x_val = item[`${kset.dataIndex}_X`];
|
||||
// const _title = kset.title.replace('-[object Object]', '');
|
||||
const v = { [kset.title]: x_val || data_val || render_val };
|
||||
return { ...sv, ...v };
|
||||
}, {});
|
||||
return itemMapped;
|
||||
});
|
||||
const ws = utils.json_to_sheet([].concat(isEmpty(summaryRow) ? [] : [summaryRow], data), { header: columnsMap.filter((r) => r.dataIndex).map((r) => r.title) });
|
||||
const wb = utils.book_new();
|
||||
utils.book_append_sheet(wb, ws, 'sheet');
|
||||
writeFile(wb, `${output_name}.xlsx`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
icon={<DownloadOutlined />}
|
||||
size="small"
|
||||
disabled={false}
|
||||
onClick={onExport}
|
||||
>
|
||||
{props.btnTxt || '导出excel'}
|
||||
</Button>
|
||||
);
|
||||
};
|
@ -0,0 +1,60 @@
|
||||
import { Component } from 'react';
|
||||
import { Select } from 'antd';
|
||||
// import { groups, leafGroup } from '../../libs/ht';
|
||||
|
||||
/**
|
||||
* 小组
|
||||
*/
|
||||
export const groups = [
|
||||
{ value: '1,2,28,7,33', key: '1,2,28,7,33', label: 'GH事业部', code: 'GH', children: [1, 2, 28, 7, 33] },
|
||||
{ value: '8,9,11,12,20,21', key: '8,9,11,12,20,21', label: '国际事业部', code: 'INT', children: [8, 9, 11, 12, 20, 21] },
|
||||
{ value: '10,18,16,30', key: '10,18,16,30', label: '孵化学院', code: '', children: [10, 18, 16, 30] },
|
||||
{ value: '1', key: '1', label: 'CH直销', code: '', children: [] },
|
||||
{ value: '2', key: '2', label: 'CH大客户', code: '', children: [] },
|
||||
{ value: '28', key: '28', label: 'AH亚洲项目组', code: 'AH', children: [] },
|
||||
{ value: '33', key: '33', label: 'GH项目组', code: '', children: [] },
|
||||
{ value: '7', key: '7', label: '市场推广', code: '', children: [] },
|
||||
{ value: '8', key: '8', label: '德语', code: '', children: [] },
|
||||
{ value: '9', key: '9', label: '日语', code: '', children: [] },
|
||||
{ value: '11', key: '11', label: '法语', code: '', children: [] },
|
||||
{ value: '12', key: '12', label: '西语', code: '', children: [] },
|
||||
{ value: '20', key: '20', label: '俄语', code: '', children: [] },
|
||||
{ value: '21', key: '21', label: '意语', code: '', children: [] },
|
||||
{ value: '10', key: '10', label: '商旅', code: '', children: [] },
|
||||
{ value: '18', key: '18', label: 'CT', code: 'CT', children: [] },
|
||||
{ value: '16', key: '16', label: 'APP', code: 'APP', children: [] },
|
||||
{ value: '30', key: '30', label: 'Trippest', code: 'TP', children: [] },
|
||||
{ value: '31', key: '31', label: '花梨鹰', code: '', children: [] },
|
||||
];
|
||||
export const groupsMappedByCode = groups.reduce((a, c) => ({ ...a, [String(c.code || c.key)]: c }), {});
|
||||
export const groupsMappedByKey = groups.reduce((a, c) => ({ ...a, [String(c.key)]: c }), {});
|
||||
export const leafGroup = groups.slice(3);
|
||||
export const overviewGroup = groups.slice(0, 3); // todo: 花梨鹰 APP Trippest
|
||||
|
||||
export const DeptSelector = ({show_all, isLeaf,...props}) => {
|
||||
const _show_all = ['tags', 'multiple'].includes(props.mode) ? false : show_all;
|
||||
const options = isLeaf===true ? leafGroup : groups;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
mode={props.mode}
|
||||
placeholder="选择小组"
|
||||
labelInValue
|
||||
maxTagCount={1}
|
||||
allowClear={props.mode != null}
|
||||
{...props}
|
||||
options={options}
|
||||
/>
|
||||
{/* {_show_all ? (
|
||||
<Select.Option key="ALL" value="ALL">
|
||||
所有小组
|
||||
</Select.Option>
|
||||
) : (
|
||||
''
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
|
||||
};
|
||||
export default DeptSelector;
|
@ -0,0 +1,13 @@
|
||||
import { useRouteError } from 'react-router-dom'
|
||||
import { Result } from 'antd'
|
||||
|
||||
export default function ErrorPage() {
|
||||
const errorResponse = useRouteError()
|
||||
return (
|
||||
<Result
|
||||
status='404'
|
||||
title='Sorry, an unexpected error has occurred.'
|
||||
subTitle={errorResponse?.message || errorResponse.error?.message}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { Select } from 'antd';
|
||||
import { useProductsTypes } from '@/hooks/useProductsSets';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ProductsTypesSelector = ({...props}) => {
|
||||
const productsTypes = useProductsTypes();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Select labelInValue allowClear placeholder={t('products:ProductType')} options={productsTypes} {...props}/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ProductsTypesSelector;
|
@ -0,0 +1,22 @@
|
||||
import { Result } from 'antd'
|
||||
import { usingStorage } from '@/hooks/usingStorage'
|
||||
import useAuthStore from '@/stores/Auth'
|
||||
|
||||
export default function RequireAuth({ children, ...props }) {
|
||||
|
||||
const [isPermitted, currentUser] = useAuthStore(state => [state.isPermitted, state.currentUser])
|
||||
const { userId } = usingStorage()
|
||||
|
||||
if (isPermitted(props.subject)) {
|
||||
// if (props.subject === '/account/management1') {
|
||||
return children
|
||||
} else if (props.result) {
|
||||
return (
|
||||
<Result
|
||||
status='403'
|
||||
title='403'
|
||||
subTitle={`抱歉,你(${currentUser.username})没有权限使用该功能(${props.subject})。`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import { Select, Spin } from 'antd';
|
||||
import { debounce, objectMapper } from '@/utils/commons';
|
||||
|
||||
function DebounceSelect({ fetchOptions, debounceTimeout = 800, ...props }) {
|
||||
const [fetching, setFetching] = useState(false);
|
||||
const [options, setOptions] = useState([]);
|
||||
const fetchRef = useRef(0);
|
||||
const debounceFetcher = useMemo(() => {
|
||||
const loadOptions = (value) => {
|
||||
fetchRef.current += 1;
|
||||
const fetchId = fetchRef.current;
|
||||
setOptions([]);
|
||||
setFetching(true);
|
||||
fetchOptions(value).then((newOptions) => {
|
||||
const mapperOptions = newOptions.map(ele => objectMapper(ele, props.map));
|
||||
if (fetchId !== fetchRef.current) {
|
||||
// for fetch callback order
|
||||
return;
|
||||
}
|
||||
setOptions(mapperOptions);
|
||||
setFetching(false);
|
||||
});
|
||||
};
|
||||
return debounce(loadOptions, debounceTimeout);
|
||||
}, [fetchOptions, debounceTimeout]);
|
||||
return (
|
||||
<Select
|
||||
labelInValue
|
||||
filterOption={false}
|
||||
showSearch
|
||||
allowClear
|
||||
maxTagCount={1}
|
||||
dropdownStyle={{width: '20rem'}}
|
||||
{...props}
|
||||
onSearch={debounceFetcher}
|
||||
notFoundContent={fetching ? <Spin size='small' /> : null}
|
||||
optionFilterProp='label'
|
||||
>
|
||||
{options.map((d) => (
|
||||
<Select.Option key={d.value} title={d.label}>
|
||||
{d.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export default DebounceSelect;
|
@ -0,0 +1,31 @@
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
import { Layout, Flex, theme, Spin, Divider } from 'antd';
|
||||
import BackBtn from './BackBtn';
|
||||
|
||||
const { Content, Header } = Layout;
|
||||
const HeaderWrapper = ({ children, header, loading, backTo, ...props }) => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
token: { colorBgContainer },
|
||||
} = theme.useToken();
|
||||
return (
|
||||
<>
|
||||
<Spin spinning={loading || false} wrapperClassName='h-full [&_.ant-spin-container]:h-full' >
|
||||
<Layout className=' bg-white h-full'>
|
||||
<Header className='header px-6 h-10 ' style={{ background: 'white' }}>
|
||||
<Flex justify={'space-between'} align={'center'} className='h-full'>
|
||||
{/* {header} */}
|
||||
<div className='grow h-full'>{header}</div>
|
||||
{backTo!==false && <BackBtn to={backTo} />}
|
||||
</Flex>
|
||||
</Header>
|
||||
<Divider className='my-2' />
|
||||
<Content className='overflow-auto' style={{ backgroundColor: colorBgContainer }}>
|
||||
{children || <Outlet />}
|
||||
</Content>
|
||||
</Layout>
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default HeaderWrapper;
|
@ -0,0 +1,29 @@
|
||||
import { createContext, useEffect, useState } from 'react';
|
||||
import {} from 'antd';
|
||||
import SearchInput from './SearchInput';
|
||||
import { fetchJSON } from '@/utils/request';
|
||||
import { HT_HOST } from '@/config';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
//供应商列表
|
||||
export const fetchVendorList = async (q) => {
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/VendorList`, { q });
|
||||
return errcode !== 0 ? [] : result;
|
||||
};
|
||||
|
||||
const VendorSelector = ({ ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<SearchInput
|
||||
placeholder={t('products:Vendor')}
|
||||
mode={'multiple'}
|
||||
maxTagCount={0}
|
||||
{...props}
|
||||
fetchOptions={fetchVendorList}
|
||||
map={{ travel_agency_name: 'label', travel_agency_id: 'value' }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default VendorSelector;
|
@ -0,0 +1,76 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import dayjs from "dayjs";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export const useDatePresets = () => {
|
||||
const [presets, setPresets] = useState([]);
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const newPresets = [
|
||||
{
|
||||
label: t("datetime.thisWeek"),
|
||||
value: [dayjs().startOf("w"), dayjs().endOf("w")],
|
||||
},
|
||||
{
|
||||
label: t("datetime.lastWeek"),
|
||||
value: [dayjs().startOf("w").subtract(7, "days"), dayjs().endOf("w").subtract(7, "days")],
|
||||
},
|
||||
{
|
||||
label: t("datetime.thisMonth"),
|
||||
value: [dayjs().startOf("M"), dayjs().endOf("M")],
|
||||
},
|
||||
{
|
||||
label: t("datetime.lastMonth"),
|
||||
value: [dayjs().subtract(1, "M").startOf("M"), dayjs().subtract(1, "M").endOf("M")],
|
||||
},
|
||||
{
|
||||
label: t("datetime.nextMonth"),
|
||||
value: [dayjs().add(1, "M").startOf("M"), dayjs().add(1, "M").endOf("M")],
|
||||
},
|
||||
{
|
||||
label: t("datetime.lastThreeMonth"),
|
||||
value: [dayjs().subtract(2, "M").startOf("M"), dayjs().endOf("M")],
|
||||
},
|
||||
{
|
||||
label: t("datetime.nextThreeMonth"),
|
||||
value: [dayjs().startOf("M"), dayjs().add(3,"M").endOf("M")],
|
||||
},
|
||||
{
|
||||
label: t("datetime.firstHalfYear"),
|
||||
value: [dayjs().startOf("y"), dayjs().endOf("y").subtract(6, "M")],
|
||||
},
|
||||
{
|
||||
label: t("datetime.latterHalfYear"),
|
||||
value: [dayjs().startOf("y").add(6,"M"), dayjs().endOf("y")],
|
||||
},
|
||||
{
|
||||
label: t("datetime.thisYear"),
|
||||
value: [dayjs().startOf("y"), dayjs().endOf("y")],
|
||||
},
|
||||
];
|
||||
setPresets(newPresets);
|
||||
}, [i18n.language]);
|
||||
|
||||
return presets;
|
||||
}
|
||||
|
||||
export const useWeekdays = () => {
|
||||
const [data, setData] = useState([]);
|
||||
const { t, i18n } = useTranslation();
|
||||
useEffect(() => {
|
||||
const newData = [
|
||||
{ value: '1', label: t('weekdays.1') },
|
||||
{ value: '2', label: t('weekdays.2') },
|
||||
{ value: '3', label: t('weekdays.3') },
|
||||
{ value: '4', label: t('weekdays.4') },
|
||||
{ value: '5', label: t('weekdays.5') },
|
||||
{ value: '6', label: t('weekdays.6') },
|
||||
{ value: '7', label: t('weekdays.7') },
|
||||
];
|
||||
setData(newData);
|
||||
return () => {};
|
||||
}, [i18n.language]);
|
||||
return data;
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
export const useHTLanguageSets = () => {
|
||||
const newData = [
|
||||
{ key: '1', value: '1', label: 'English' },
|
||||
{ key: '2', value: '2', label: 'Chinese (中文)' },
|
||||
{ key: '3', value: '3', label: 'Japanese (日本語)' },
|
||||
{ key: '4', value: '4', label: 'German (Deutsch)' },
|
||||
{ key: '5', value: '5', label: 'French (Français)' },
|
||||
{ key: '6', value: '6', label: 'Spanish (Español)' },
|
||||
{ key: '7', value: '7', label: 'Russian (Русский)' },
|
||||
{ key: '8', value: '8', label: 'Italian (Italiano)' },
|
||||
];
|
||||
|
||||
return newData;
|
||||
};
|
||||
|
||||
export const useHTLanguageSetsMapVal = () => {
|
||||
const stateSets = useHTLanguageSets();
|
||||
const stateMapVal = stateSets.reduce((r, c) => ({ ...r, [`${c.value}`]: c }), {});
|
||||
return stateMapVal;
|
||||
};
|
@ -0,0 +1,184 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useAuthStore from '@/stores/Auth';
|
||||
import { PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT } from '@/config';
|
||||
import { isEmpty } from '@/utils/commons';
|
||||
|
||||
/**
|
||||
* 产品管理 相关的预设数据
|
||||
* 项目类型
|
||||
* * 酒店预定 1
|
||||
* * 火车 2
|
||||
* * 飞机票务 3
|
||||
* * 游船 4
|
||||
* * 快巴 5
|
||||
* * 旅行社(综费) 6
|
||||
* * 景点 7
|
||||
* * 特殊项目 8
|
||||
* * 其他 9
|
||||
* * 酒店 A
|
||||
* * 超公里 B
|
||||
* * 餐费 C
|
||||
* * 小包价 D // 包价线路
|
||||
* * 站 X
|
||||
* * 购物 S
|
||||
* * 餐 R (餐厅)
|
||||
* * 娱乐 E
|
||||
* * 精华线路 T
|
||||
* * 客人testimonial F
|
||||
* * 线路订单 O
|
||||
* * 省 P
|
||||
* * 信息 I
|
||||
* * 国家 G
|
||||
* * 城市 K
|
||||
* * 图片 H
|
||||
* * 地图 M
|
||||
* * 包价线路 L (已废弃)
|
||||
* * 节日节庆 V
|
||||
* * 火车站 N
|
||||
* * 手机租赁 Z
|
||||
* * ---- webht 类型, 20240624 新增HT类型 ----
|
||||
* * 导游 Q
|
||||
* * 车费 J
|
||||
*/
|
||||
|
||||
export const useProductsTypes = (showAll = false) => {
|
||||
const [types, setTypes] = useState([]);
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const allItem = [{ label: t('All'), value: '', key: '' }];
|
||||
const newData = [
|
||||
{ label: t('products:type.Experience'), value: '6', key: '6' },
|
||||
{ label: t('products:type.UltraService'), value: 'B', key: 'B' },
|
||||
{ label: t('products:type.Car'), value: 'J', key: 'J' },
|
||||
{ label: t('products:type.Guide'), value: 'Q', key: 'Q' },
|
||||
{ label: t('products:type.Attractions'), value: '7', key: '7' }, // landscape
|
||||
{ label: t('products:type.Meals'), value: 'R', key: 'R' },
|
||||
{ label: t('products:type.Extras'), value: '8', key: '8' },
|
||||
{ label: t('products:type.Package'), value: 'D', key: 'D' },
|
||||
];
|
||||
const res = showAll ? [...allItem, ...newData] : newData;
|
||||
setTypes(res);
|
||||
}, [i18n.language]);
|
||||
|
||||
return types;
|
||||
};
|
||||
export const useProductsTypesMapVal = (value) => {
|
||||
const stateSets = useProductsTypes();
|
||||
const stateMapVal = stateSets.reduce((r, c) => ({ ...r, [`${c.value}`]: c }), {});
|
||||
return stateMapVal;
|
||||
};
|
||||
|
||||
export const useProductsAuditStates = () => {
|
||||
const [types, setTypes] = useState([]);
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const newData = [
|
||||
{ key: '-1', value: '-1', label: t('products:auditState.New'), color: 'muted' },
|
||||
{ key: '0', value: '0', label: t('products:auditState.Pending'), color: '' },
|
||||
{ key: '2', value: '2', label: t('products:auditState.Approved'), color: 'primary' },
|
||||
{ key: '3', value: '3', label: t('products:auditState.Rejected'), color: 'danger' },
|
||||
{ key: '1', value: '1', label: t('products:auditState.Published'), color: 'primary' },
|
||||
// ELSE 未知
|
||||
];
|
||||
setTypes(newData);
|
||||
}, [i18n.language]);
|
||||
|
||||
return types;
|
||||
};
|
||||
|
||||
export const useProductsAuditStatesMapVal = (value) => {
|
||||
const stateSets = useProductsAuditStates();
|
||||
const stateMapVal = stateSets.reduce((r, c) => ({ ...r, [`${c.value}`]: c }), {});
|
||||
return stateMapVal;
|
||||
};
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export const useProductsTypesFieldsets = (type) => {
|
||||
const [isPermitted] = useAuthStore((state) => [state.isPermitted]);
|
||||
const infoDefault = [['city'], ['title']];
|
||||
const infoAdmin = ['title', 'product_title', 'code', 'remarks', 'dept']; // 'display_to_c'
|
||||
const infoDisplay = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['display_to_c'] : [];
|
||||
const infoRecDisplay = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['recommends_rate'] : [];
|
||||
const infoTypesMap = {
|
||||
'6': [[...infoDisplay], []],
|
||||
'B': [['km', ...infoDisplay], []],
|
||||
'J': [[...infoRecDisplay, 'duration', ...infoDisplay], ['description']],
|
||||
'Q': [[...infoRecDisplay, 'duration', ...infoDisplay], ['description']],
|
||||
'D': [[...infoRecDisplay, 'duration', ...infoDisplay], ['description']],
|
||||
'7': [[...infoRecDisplay, 'duration', 'open_weekdays', ...infoDisplay], ['description']],
|
||||
'R': [[...infoDisplay], ['description']],
|
||||
'8': [[...infoDisplay], []],
|
||||
};
|
||||
const thisTypeFieldset = (_type) => {
|
||||
if (isEmpty(_type)) {
|
||||
return infoDefault;
|
||||
}
|
||||
const adminSet = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? infoAdmin : [];
|
||||
return [
|
||||
[...infoDefault[0], ...infoTypesMap[_type][0], ...adminSet],
|
||||
[...infoDefault[1], ...infoTypesMap[_type][1]],
|
||||
];
|
||||
};
|
||||
return thisTypeFieldset(type);
|
||||
};
|
||||
|
||||
export const useNewProductRecord = () => {
|
||||
return {
|
||||
info: {
|
||||
'id': '',
|
||||
'htid': 0,
|
||||
'title': '',
|
||||
'code': '',
|
||||
'product_type_id': '',
|
||||
'product_type_name': '',
|
||||
'remarks': '',
|
||||
'duration': 0,
|
||||
'duration_unit': 'h',
|
||||
'open_weekdays': ['1', '2', '3', '4', '5', '6', '7'],
|
||||
'recommends_rate': 0,
|
||||
'dept_id': 0,
|
||||
'dept_name': '',
|
||||
'display_to_c': '0',
|
||||
'km': 0,
|
||||
'city_id': 0,
|
||||
'city_name': '',
|
||||
'open_hours': '',
|
||||
'lastedit_changed': '',
|
||||
'create_date': '',
|
||||
'created_by': '',
|
||||
'edit_status': 2,
|
||||
},
|
||||
lgc_details: [
|
||||
{
|
||||
'title': '',
|
||||
'descriptions': '',
|
||||
'lgc': 1,
|
||||
'id': '',
|
||||
'edit_status': 2,
|
||||
},
|
||||
],
|
||||
quotation: [
|
||||
{
|
||||
'id': '',
|
||||
'adult_cost': 0,
|
||||
'child_cost': 0,
|
||||
'currency': 'RMB',
|
||||
'unit_id': '1',
|
||||
'unit_name': '每团',
|
||||
'group_size_min': 1,
|
||||
'group_size_max': 2,
|
||||
'use_dates_start': '',
|
||||
'use_dates_end': '',
|
||||
'weekdays': '',
|
||||
'audit_state_id': -1,
|
||||
'audit_state_name': '',
|
||||
'lastedit_changed': 'new',
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
@ -0,0 +1,86 @@
|
||||
const persistObject = {}
|
||||
|
||||
/**
|
||||
* G-INT:USER_ID -> userId = 456
|
||||
* G-STR:LOGIN_TOKEN -> loginToken = 'E6779386E7D64DF0ADD0F97767E00D8B'
|
||||
* G-JSON:LOGIN_USER -> loginUser = { username: 'test-username' }
|
||||
*/
|
||||
export function usingStorage() {
|
||||
|
||||
const getStorage = () => {
|
||||
if (import.meta.env.DEV && window.localStorage) {
|
||||
return window.localStorage
|
||||
} else if (window.sessionStorage) {
|
||||
return window.sessionStorage
|
||||
} else {
|
||||
console.error('browser not support localStorage and sessionStorage.')
|
||||
}
|
||||
}
|
||||
|
||||
const setProperty = (key, value) => {
|
||||
const webStorage = getStorage()
|
||||
const typeAndKey = key.split(':')
|
||||
if (typeAndKey.length === 2) {
|
||||
const propName = camelCasedWords(typeAndKey[1])
|
||||
persistObject[propName] = value
|
||||
if (typeAndKey[0] === 'G-JSON') {
|
||||
webStorage.setItem(key, JSON.stringify(value))
|
||||
} else {
|
||||
webStorage.setItem(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// USER_ID -> userId
|
||||
const camelCasedWords = (string) => {
|
||||
if (typeof string !== 'string' || string.length === 0) {
|
||||
return string;
|
||||
}
|
||||
return string.split('_').map((word, index) => {
|
||||
if (index === 0) {
|
||||
return word.toLowerCase()
|
||||
} else {
|
||||
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
||||
}
|
||||
}).join('')
|
||||
}
|
||||
|
||||
if (Object.keys(persistObject).length == 0) {
|
||||
|
||||
const webStorage = getStorage()
|
||||
|
||||
for (let i = 0; i < webStorage.length; i++) {
|
||||
const key = webStorage.key(i)
|
||||
const typeAndKey = key.split(':')
|
||||
|
||||
if (typeAndKey.length === 2) {
|
||||
const value = webStorage.getItem(key)
|
||||
const propName = camelCasedWords(typeAndKey[1])
|
||||
if (typeAndKey[0] === 'G-INT') {
|
||||
persistObject[propName] = parseInt(value, 10)
|
||||
} else if (typeAndKey[0] === 'G-JSON') {
|
||||
try {
|
||||
persistObject[propName] = JSON.parse(value)
|
||||
} catch (e) {
|
||||
// 如果解析失败,保留原始字符串值
|
||||
persistObject[propName] = value
|
||||
console.error('解析 JSON 失败。')
|
||||
}
|
||||
} else {
|
||||
persistObject[propName] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...persistObject,
|
||||
setStorage: (key, value) => {
|
||||
setProperty(key, value)
|
||||
},
|
||||
clearStorage: () => {
|
||||
getStorage().clear()
|
||||
Object.assign(persistObject, {})
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Dropdown } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { appendRequestParams } from '@/utils/request';
|
||||
|
||||
const i18n_to_htcode = {
|
||||
'zh': 2,
|
||||
'en': 1,
|
||||
};
|
||||
|
||||
export const useDefaultLgc = () => {
|
||||
const { i18n } = useTranslation();
|
||||
return { language: i18n_to_htcode[i18n.language], };
|
||||
};
|
||||
/**
|
||||
* 语言选择组件
|
||||
*/
|
||||
const Language = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [selectedKeys, setSelectedKeys] = useState([i18n.language]);
|
||||
|
||||
useEffect(() => {
|
||||
appendRequestParams('lgc', i18n_to_htcode[i18n.language]);
|
||||
|
||||
return () => {};
|
||||
}, [i18n.language]);
|
||||
|
||||
// 切换语言事件
|
||||
const handleChangeLanguage = ({ key }) => {
|
||||
setSelectedKeys([key]);
|
||||
i18n.changeLanguage(key);
|
||||
};
|
||||
|
||||
const langSupports = ['en', 'zh'].map((lang) => ({ label: t(`lang.${lang}`), key: lang }));
|
||||
|
||||
/* 🌏🌐 */
|
||||
return (
|
||||
<Dropdown menu={{ items: langSupports, onClick: handleChangeLanguage, style: { width: 100 }, selectedKeys: selectedKeys }}>
|
||||
<div className='icon text-primary'>🌐<span>{t(`lang.${i18n.language}`)}</span></div>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default Language;
|
@ -0,0 +1,56 @@
|
||||
import i18n from 'i18next';
|
||||
// 用于检测浏览器中的用户语言,
|
||||
// https://github.com/i18next/i18next-browser-languageDetector
|
||||
// 通过localStorage.getItem('i18nextLng')取出当前语言环境
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
// import en from './locales/en.json';
|
||||
// import zh from './locales/zh.json';
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
// https://www.i18next.com/overview/configuration-options
|
||||
.init({
|
||||
backend: {
|
||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||
},
|
||||
ns: ['common', 'group', 'vendor', 'account', 'products'],
|
||||
defaultNS: 'common',
|
||||
detection: {
|
||||
// convertDetectedLanguage: 'Iso15897',
|
||||
convertDetectedLanguage: (lng) => {
|
||||
const langPart = lng.split('-')[0];
|
||||
return langPart;
|
||||
// return lng.replace('-', '_');
|
||||
},
|
||||
},
|
||||
supportedLngs: ['en', 'zh'],
|
||||
// resources: {
|
||||
// en: { translation: en },
|
||||
// zh: { translation: zh },
|
||||
// },
|
||||
fallbackLng: 'en',
|
||||
// fallbackLng: (code) => {
|
||||
// if (!code || code === 'en') return ['en'];
|
||||
// const fallbacks = []; // [code];
|
||||
|
||||
// // add pure lang
|
||||
// const langPart = code.split('-')[0];
|
||||
// if (langPart !== code) fallbacks.push(langPart);
|
||||
|
||||
// fallbacks.push('en');
|
||||
// console.log('fallbacks', fallbacks);
|
||||
// return fallbacks;
|
||||
// },
|
||||
preload: ['en'],
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
// keySeparator: false,
|
||||
debug: false,
|
||||
});
|
||||
|
||||
export default i18n;
|
@ -0,0 +1,46 @@
|
||||
## 目录结构
|
||||
|
||||
├── locales
|
||||
│ ├── en
|
||||
│ │ ├── common.json
|
||||
│ │ ├── [ns].json
|
||||
│ ├── zh
|
||||
│ │ ├── common.json
|
||||
│ │ ├── [ns].json
|
||||
|
||||
1. 翻译的资源文件位于 public/locales 文件夹,文件夹结构是 [语言] > [命名空间].json.
|
||||
[语言]命名使用短格式, 暂不考虑区分地区差异. 如: en-GB, en-US, en-AU 均使用`en`
|
||||
2. common.json 是公共的 json 文件,存放一些公共的 key-value
|
||||
3. [ns].json 是命名空间 json 文件,存放一些命名空间的 key-value
|
||||
4. 命名空间根据自己的需求/功能模块来创建,比如:`group`(团计划), `feedback`(反馈表)等
|
||||
5. json 文件中的 key 使用驼峰命名
|
||||
|
||||
⭐DEV
|
||||
|
||||
1. 翻译的资源文件位于 public 下, 切换语言时异步请求.
|
||||
2. 开发过程编辑了 locales/\*.json 后, 需要刷新页面才会生效. 热加载不生效
|
||||
3. 新增的命名空间, 添加到 src/i18n/index.js 中配置的 `init({ ns })` 中
|
||||
|
||||
⭐⭐ 组件中使用
|
||||
|
||||
1. 默认 common 命名空间
|
||||
2. 取值: `t('[ns]:[json_path]')`, json path 的层级使用`.`分隔
|
||||
|
||||
```js
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const { t } = useTranslation();
|
||||
// const { t, i18n } = useTranslation('group'); // 指定命名空间
|
||||
|
||||
// 默认common命名空间
|
||||
<button >{t('Search')}</button>
|
||||
<button >{t('datetime.thisWeek')}</button>
|
||||
|
||||
// 命名空间取值
|
||||
<button >{t('group:ArrivalDate')}</button>
|
||||
|
||||
```
|
||||
|
||||
⭐⭐⭐ 格式: 日期, 金额等
|
||||
|
||||
[文档 Formatting](https://www.i18next.com/translation-function/formatting)
|
@ -1,85 +1,132 @@
|
||||
import React from "react";
|
||||
import { configure } from "mobx";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import {
|
||||
createBrowserRouter,
|
||||
RouterProvider,
|
||||
} from "react-router-dom";
|
||||
import RootStore from "@/stores/Root";
|
||||
import { StoreContext } from '@/stores/StoreContext';
|
||||
import "@/assets/global.css";
|
||||
import App from "@/views/App";
|
||||
import Standlone from "@/views/Standlone";
|
||||
import Login from "@/views/Login";
|
||||
import Index from "@/views/index";
|
||||
import ErrorPage from "@/views/error-page";
|
||||
import ReservationNewest from "@/views/reservation/Newest";
|
||||
import ReservationDetail from "@/views/reservation/Detail";
|
||||
import ChangePassword from "@/views/account/ChangePassword";
|
||||
import AccountProfile from "@/views/account/Profile";
|
||||
import FeedbackIndex from "@/views/feedback/Index";
|
||||
import FeedbackDetail from "@/views/feedback/Detail";
|
||||
import FeedbackCustomerDetail from "@/views/feedback/CustomerDetail";
|
||||
import ReportIndex from "@/views/report/Index";
|
||||
import NoticeIndex from "@/views/notice/Index";
|
||||
import NoticeDetail from "@/views/notice/Detail";
|
||||
import InvoiceIndex from "@/views/invoice/Index";
|
||||
import InvoiceDetail from "@/views/invoice/Detail";
|
||||
import InvoicePaid from "@/views/invoice/Paid";
|
||||
import InvoicePaidDetail from "@/views/invoice/PaidDetail";
|
||||
import ChangeVendor from "@/views/account/ChangeVendor";
|
||||
|
||||
|
||||
configure({
|
||||
useProxies: "ifavailable",
|
||||
enforceActions: "observed",
|
||||
computedRequiresReaction: true,
|
||||
observableRequiresReaction: false,
|
||||
reactionRequiresObservable: true,
|
||||
disableErrorBoundaries: process.env.NODE_ENV == "production"
|
||||
});
|
||||
|
||||
const router = createBrowserRouter([
|
||||
} from 'react-router-dom'
|
||||
import '@/assets/global.css'
|
||||
import App from '@/views/App'
|
||||
import Standlone from '@/views/Standlone'
|
||||
import Login from '@/views/Login'
|
||||
import Logout from '@/views/Logout'
|
||||
import ErrorPage from '@/components/ErrorPage'
|
||||
import RequireAuth from '@/components/RequireAuth'
|
||||
import ReservationNewest from '@/views/reservation/Newest'
|
||||
import ReservationDetail from '@/views/reservation/Detail'
|
||||
import ChangePassword from '@/views/account/ChangePassword'
|
||||
import AccountProfile from '@/views/account/Profile'
|
||||
import AccountManagement from '@/views/account/Management'
|
||||
import RoleList from '@/views/account/RoleList'
|
||||
import FeedbackIndex from '@/views/feedback/Index'
|
||||
import FeedbackDetail from '@/views/feedback/Detail'
|
||||
import FeedbackCustomerDetail from '@/views/feedback/CustomerDetail'
|
||||
import ReportIndex from '@/views/report/Index'
|
||||
import NoticeIndex from '@/views/notice/Index'
|
||||
import NoticeDetail from '@/views/notice/Detail'
|
||||
import InvoiceIndex from '@/views/invoice/Index'
|
||||
import InvoiceDetail from '@/views/invoice/Detail'
|
||||
import InvoiceHistory from '@/views/invoice/History'
|
||||
import InvoicePaid from '@/views/invoice/Paid'
|
||||
import InvoicePaidDetail from '@/views/invoice/PaidDetail'
|
||||
import Airticket from '@/views/airticket/Index'
|
||||
import AirticketPlan from '@/views/airticket/Plan'
|
||||
import AirticketInvoice from '@/views/airticket/Invoice'
|
||||
import AirticketInvoicePaid from '@/views/airticket/InvoicePaid'
|
||||
|
||||
import Trainticket from '@/views/trainticket/index'
|
||||
import TrainticketPlan from '@/views/trainticket/plan'
|
||||
import TrainticketInvoice from '@/views/trainticket/invoice'
|
||||
import TrainticketInvoicePaid from '@/views/trainticket/invoicePaid'
|
||||
|
||||
import { ThemeContext } from '@/stores/ThemeContext'
|
||||
import { usingStorage } from '@/hooks/usingStorage'
|
||||
import useAuthStore from './stores/Auth'
|
||||
import { isNotEmpty } from '@/utils/commons'
|
||||
|
||||
import ProductsManage from '@/views/products/Manage';
|
||||
import ProductsDetail from '@/views/products/Detail';
|
||||
import ProductsAudit from '@/views/products/Audit';
|
||||
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA,PERM_TRAIN_TICKET, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_PUT } from '@/config'
|
||||
|
||||
import './i18n'
|
||||
|
||||
const initRouter = async () => {
|
||||
return createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
path: '/',
|
||||
element: <App />,
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
{ index: true, element: <Index /> },
|
||||
{ path: "reservation/newest", element: <ReservationNewest />},
|
||||
{ path: "reservation/:reservationId", element: <ReservationDetail />},
|
||||
{ path: "account/change-password", element: <ChangePassword />},
|
||||
{ path: "account/profile", element: <AccountProfile />},
|
||||
{ path: "feedback", element: <FeedbackIndex />},
|
||||
{ path: "feedback/:GRI_SN/:RefNo", element: <FeedbackDetail />},
|
||||
{ path: "feedback/:GRI_SN/:CII_SN/:RefNo", element: <FeedbackCustomerDetail />},
|
||||
{ path: "report", element: <ReportIndex />},
|
||||
{ path: "notice", element: <NoticeIndex />},
|
||||
{ path: "notice/:CCP_BLID", element: <NoticeDetail />},
|
||||
{ path: "invoice",element:<InvoiceIndex />},
|
||||
{ path: "invoice/detail/:GMDSN/:GSN",element:<InvoiceDetail />},
|
||||
{ path: "invoice/paid",element:<InvoicePaid />},
|
||||
{ path: "invoice/paid/detail/:flid",element:<InvoicePaidDetail />},
|
||||
{ path: "account/change-vendor",element:<ChangeVendor />},
|
||||
{ index: true, element: <NoticeIndex /> },
|
||||
{ path: 'account/change-password', element: <ChangePassword />},
|
||||
{ path: 'account/profile', element: <AccountProfile />},
|
||||
{ path: 'account/management', element: <RequireAuth subject={PERM_ACCOUNT_MANAGEMENT} result={true}><AccountManagement /></RequireAuth>},
|
||||
{ path: 'account/role-list', element: <RequireAuth subject={PERM_ROLE_NEW} result={true}><RoleList /></RequireAuth>},
|
||||
{ path: 'reservation/newest', element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReservationNewest /></RequireAuth>},
|
||||
{ path: 'reservation/:reservationId', element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReservationDetail /></RequireAuth>},
|
||||
{ path: 'feedback', element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackIndex /></RequireAuth>},
|
||||
{ path: 'feedback/:GRI_SN/:CII_SN/:RefNo', element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackCustomerDetail /></RequireAuth>},
|
||||
{ path: 'feedback/:GRI_SN/:RefNo', element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackDetail /></RequireAuth>},
|
||||
{ path: 'report', element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReportIndex /></RequireAuth>},
|
||||
{ path: 'notice', element: <NoticeIndex />},
|
||||
{ path: 'notice/:CCP_BLID', element: <NoticeDetail />},
|
||||
{ path: 'invoice',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceIndex /></RequireAuth>},
|
||||
{ path: 'invoice/detail/:GMDSN/:GSN',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceDetail /></RequireAuth>},
|
||||
{ path: 'invoice/history/:GMDSN/:GSN',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceHistory /></RequireAuth>},
|
||||
{ path: 'invoice/paid',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaid /></RequireAuth>},
|
||||
{ path: 'invoice/paid/detail/:flid', element: <RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaidDetail /></RequireAuth>},
|
||||
{ path: 'airticket',element: <RequireAuth subject={PERM_AIR_TICKET} result={true}><Airticket /></RequireAuth>},
|
||||
{ path: 'airticket/plan/:coli_sn/:gri_sn',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketPlan /></RequireAuth>},
|
||||
{ path: 'airticket/invoice',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketInvoice /></RequireAuth>},
|
||||
{ path: 'airticket/invoicepaid',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketInvoicePaid /></RequireAuth>},
|
||||
|
||||
{ path: 'trainticket',element: <RequireAuth subject={PERM_TRAIN_TICKET} result={true}><Trainticket /></RequireAuth>},
|
||||
{ path: 'trainticket/plan/:coli_sn/:gri_sn',element:<RequireAuth subject={PERM_TRAIN_TICKET} result={true}><TrainticketPlan /></RequireAuth>},
|
||||
{ path: 'trainticket/invoice',element:<RequireAuth subject={PERM_TRAIN_TICKET} result={true}><TrainticketInvoice /></RequireAuth>},
|
||||
{ path: 'trainticket/invoicepaid',element:<RequireAuth subject={PERM_TRAIN_TICKET} result={true}><TrainticketInvoicePaid /></RequireAuth>},
|
||||
|
||||
{ path: "products",element: <RequireAuth subject={PERM_PRODUCTS_MANAGEMENT} result={true}><ProductsManage /></RequireAuth>},
|
||||
{ path: "products/:travel_agency_id/:use_year/:audit_state/audit",element:<RequireAuth subject={PERM_PRODUCTS_MANAGEMENT} result={true}><ProductsAudit /></RequireAuth>},
|
||||
|
||||
{ path: "products/:travel_agency_id/:use_year/:audit_state/edit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsDetail /></RequireAuth>},
|
||||
|
||||
{ path: "products/audit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsAudit /></RequireAuth>},
|
||||
|
||||
{ path: "products/edit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsDetail /></RequireAuth>},
|
||||
]
|
||||
},
|
||||
{
|
||||
element: <Standlone />,
|
||||
children: [
|
||||
{ path: "/login", element: <Login /> },
|
||||
{ path: '/login', element: <Login /> },
|
||||
{ path: '/logout', element: <Logout /> },
|
||||
]
|
||||
}
|
||||
]);
|
||||
])
|
||||
}
|
||||
|
||||
const initAppliction = async () => {
|
||||
|
||||
const rootStore = new RootStore();
|
||||
const { loginToken, userId } = usingStorage()
|
||||
|
||||
if (isNotEmpty(userId) && isNotEmpty(loginToken)) {
|
||||
await useAuthStore.getState().initAuth()
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
const router = await initRouter()
|
||||
const root = document.getElementById('root')
|
||||
|
||||
if (!root) throw new Error('No root element found')
|
||||
|
||||
createRoot(root).render(
|
||||
//<React.StrictMode>
|
||||
<StoreContext.Provider value={rootStore}>
|
||||
<ThemeContext.Provider value={{ colorPrimary: '#00b96b', borderRadius: 4 }}>
|
||||
<RouterProvider
|
||||
router={router}
|
||||
fallbackElement={() => <div>Loading...</div>}
|
||||
/>
|
||||
</StoreContext.Provider>
|
||||
</ThemeContext.Provider>
|
||||
//</React.StrictMode>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
initAppliction()
|
||||
|
@ -0,0 +1,51 @@
|
||||
import { loadScript } from '@/utils/commons';
|
||||
import { PROJECT_NAME, BUILD_VERSION } from '@/config';
|
||||
|
||||
export const loadPageSpy = (title) => {
|
||||
|
||||
if (import.meta.env.DEV || window.$pageSpy) return
|
||||
|
||||
const PageSpyConfig = { api: 'page-spy.mycht.cn', project: PROJECT_NAME, title: title, autoRender: false };
|
||||
const PageSpySrc = [
|
||||
'https://page-spy.mycht.cn/page-spy/index.min.js'+`?${BUILD_VERSION}`,
|
||||
'https://page-spy.mycht.cn/plugin/data-harbor/index.min.js'+`?${BUILD_VERSION}`,
|
||||
'https://page-spy.mycht.cn/plugin/rrweb/index.min.js'+`?${BUILD_VERSION}`,
|
||||
];
|
||||
|
||||
Promise.all(PageSpySrc.map((src) => loadScript(src))).then(() => {
|
||||
// 注册插件
|
||||
window.$harbor = new DataHarborPlugin();
|
||||
window.$rrweb = new RRWebPlugin();
|
||||
[window.$harbor, window.$rrweb].forEach(p => {
|
||||
PageSpy.registerPlugin(p)
|
||||
})
|
||||
window.$pageSpy = new PageSpy(PageSpyConfig);
|
||||
});
|
||||
};
|
||||
|
||||
export const uploadPageSpyLog = async () => {
|
||||
// window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
|
||||
if (window.$pageSpy) {
|
||||
await window.$harbor.upload() // 上传日志 { clearCache: true, remark: '' }
|
||||
alert('Success')
|
||||
} else {
|
||||
alert('Failure')
|
||||
}
|
||||
}
|
||||
|
||||
export const PageSpyLog = () => {
|
||||
return (
|
||||
<>
|
||||
{window.$pageSpy && (
|
||||
<a
|
||||
className='text-primary'
|
||||
onClick={() => {
|
||||
window.$pageSpy.triggerPlugins('onOfflineLog', 'download');
|
||||
window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
|
||||
}}>
|
||||
上传Debug日志 ({window.$pageSpy.address.substring(0, 4)})
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,191 @@
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import { fetchJSON, postForm } from '@/utils/request'
|
||||
import { isEmpty, isNotEmpty } from '@/utils/commons'
|
||||
import { HT_HOST } from "@/config"
|
||||
import { usingStorage } from '@/hooks/usingStorage'
|
||||
|
||||
export const postAccountStatus = async (formData) => {
|
||||
|
||||
const { errcode, result } = await postForm(
|
||||
`${HT_HOST}/service-CooperateSOA/set_account_status`, formData)
|
||||
return errcode !== 0 ? {} : result
|
||||
}
|
||||
|
||||
export const postAccountPassword = async (formData) => {
|
||||
|
||||
const { errcode, result } = await postForm(
|
||||
`${HT_HOST}/service-CooperateSOA/reset_account_password`, formData)
|
||||
return errcode !== 0 ? {} : result
|
||||
}
|
||||
|
||||
export const fetchAccountList = async (params) => {
|
||||
|
||||
const { errcode, result } = await fetchJSON(
|
||||
`${HT_HOST}/service-CooperateSOA/search_account`, params)
|
||||
return errcode !== 0 ? {} : result
|
||||
}
|
||||
|
||||
export const postAccountForm = async (formData) => {
|
||||
|
||||
const { errcode, result } = await postForm(
|
||||
`${HT_HOST}/service-CooperateSOA/new_or_update_account`, formData)
|
||||
return errcode !== 0 ? {} : result
|
||||
}
|
||||
|
||||
export const postRoleForm = async (formData) => {
|
||||
|
||||
const { errcode, result } = await postForm(
|
||||
`${HT_HOST}/service-CooperateSOA/new_or_update_role`, formData)
|
||||
return errcode !== 0 ? {} : result
|
||||
}
|
||||
|
||||
export const fetchRoleList = async () => {
|
||||
|
||||
const { errcode, result } = await fetchJSON(
|
||||
`${HT_HOST}/service-CooperateSOA/get_role_list`)
|
||||
return errcode !== 0 ? {} : result
|
||||
}
|
||||
|
||||
export const fetchPermissionList = async () => {
|
||||
|
||||
const { errcode, result } = await fetchJSON(
|
||||
`${HT_HOST}/service-CooperateSOA/get_all_permission_list`)
|
||||
return errcode !== 0 ? {} : result
|
||||
}
|
||||
|
||||
export const fetchPermissionListByRoleId = async (params) => {
|
||||
|
||||
const { errcode, result } = await fetchJSON(
|
||||
`${HT_HOST}/service-CooperateSOA/get_role_permission_list`, params)
|
||||
return errcode !== 0 ? {} : result
|
||||
}
|
||||
|
||||
export const fetchTravelAgencyByName = async (name) => {
|
||||
|
||||
const { errcode, result } = await fetchJSON(
|
||||
`${HT_HOST}/Service_BaseInfoWeb/VendorList`, {q: name})
|
||||
return errcode !== 0 ? {} : result
|
||||
}
|
||||
|
||||
export const genRandomPassword = () => {
|
||||
let result = ''
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||
const charactersLength = characters.length
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength))
|
||||
}
|
||||
|
||||
result += '@' + (Math.floor(Math.random() * 900) + 100)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const useAccountStore = create(devtools((set) => ({
|
||||
|
||||
accountList: [],
|
||||
|
||||
toggleAccountStatus: async (userId, status) => {
|
||||
|
||||
const statusValue = status ? 'enable' : 'disable'
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('lmi_sn', userId)
|
||||
formData.append('account_status', statusValue)
|
||||
|
||||
return postAccountStatus(formData)
|
||||
},
|
||||
|
||||
resetAccountPassword: async (userId, password) => {
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('lmi_sn', userId)
|
||||
formData.append('newPassword', password)
|
||||
|
||||
return postAccountPassword(formData)
|
||||
},
|
||||
|
||||
newEmptyRole: () => ({
|
||||
role_id: null,
|
||||
role_name: '',
|
||||
role_ids: ''
|
||||
}),
|
||||
|
||||
newEmptyAccount: () => {
|
||||
return {
|
||||
accountId: null,
|
||||
userId: null,
|
||||
lmi2_sn: null,
|
||||
username: '',
|
||||
realname: '',
|
||||
email: '',
|
||||
travelAgencyId: null,
|
||||
roleId: ''
|
||||
}
|
||||
},
|
||||
|
||||
saveOrUpdateRole: async (formValues) => {
|
||||
const formData = new FormData()
|
||||
formData.append('role_id', formValues.role_id)
|
||||
formData.append('role_name', formValues.role_name)
|
||||
formData.append('res_ids', formValues.res_array.join(','))
|
||||
|
||||
return postRoleForm(formData)
|
||||
},
|
||||
|
||||
saveOrUpdateAccount: async (formValues) => {
|
||||
const { userId } = usingStorage()
|
||||
const formData = new FormData()
|
||||
formData.append('wu_id', formValues.accountId)
|
||||
formData.append('lmi_sn', formValues.userId)
|
||||
formData.append('lmi2_sn', formValues.lmi2_sn)
|
||||
formData.append('user_name', formValues.username)
|
||||
formData.append('real_name', formValues.realname)
|
||||
formData.append('email', formValues.email)
|
||||
formData.append('travel_agency_id', formValues.travelAgencyId)
|
||||
formData.append('roles', formValues.roleId)
|
||||
|
||||
formData.append('opi_sn', userId)
|
||||
|
||||
return postAccountForm(formData)
|
||||
},
|
||||
|
||||
searchAccountByCriteria: async (formValues) => {
|
||||
let travel_agency_ids = null
|
||||
if (isNotEmpty(formValues.agency)) {
|
||||
travel_agency_ids = formValues.agency.map((ele) => ele.key).join(',')
|
||||
}
|
||||
const searchParams = {
|
||||
username: formValues.username,
|
||||
travel_agency_ids: travel_agency_ids,
|
||||
lgc: 2
|
||||
}
|
||||
|
||||
const resultArray = await fetchAccountList(searchParams)
|
||||
|
||||
const mapAccoutList = resultArray.map((r) => {
|
||||
return {
|
||||
accountId: r.wu_id,
|
||||
userId: r.lmi_sn,
|
||||
lmi2_sn: r.lmi2_sn,
|
||||
username: r.user_name,
|
||||
realname: r.real_name,
|
||||
email: r.email,
|
||||
lastLogin: r.wu_lastlogindate,
|
||||
travelAgencyName: r.travel_agency_name,
|
||||
travelAgencyId: r.travel_agency_id,
|
||||
disabled: r.wu_limitsign,
|
||||
// 数据库支持逗号分隔多角色(5,6,7),目前界面只需单个。
|
||||
roleId: isEmpty(r.roles) ? 0 : parseInt(r.roles),
|
||||
role: r.roles_name,
|
||||
}
|
||||
})
|
||||
|
||||
set(() => ({
|
||||
accountList: mapAccoutList
|
||||
}))
|
||||
},
|
||||
}), { name: 'accountStore' }))
|
||||
|
||||
export default useAccountStore
|
@ -0,0 +1,12 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
|
||||
export const useFormStore = create(
|
||||
devtools((set, get) => ({
|
||||
formValues: {},
|
||||
setFormValues: (values) => set((state) => ({ formValues: { ...state.formValues, ...values } })),
|
||||
formValuesToSub: {},
|
||||
setFormValuesToSub: (values) => set((state) => ({ formValuesToSub: { ...state.formValuesToSub, ...values } })),
|
||||
}), { name: 'formStore' })
|
||||
);
|
||||
export default useFormStore;
|
@ -0,0 +1,453 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import dayjs from 'dayjs'
|
||||
import { fetchJSON, postForm, postJSON } from '@/utils/request';
|
||||
import { HT_HOST } from '@/config';
|
||||
import { groupBy, generateId, isNotEmpty } from '@/utils/commons';
|
||||
|
||||
export const searchAgencyAction = async (param) => {
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/products_search`, param);
|
||||
return errcode !== 0 ? [] : result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 搜索所有产品, 返回产品列表
|
||||
* ! 只有审核通过, 已发布的
|
||||
* @param {object} params { keyword, use_year, product_types, travel_agency_id, city }
|
||||
*/
|
||||
export const searchPublishedProductsAction = async (param) => {
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/web_products_search`, param);
|
||||
return errcode !== 0 ? [] : result;
|
||||
};
|
||||
|
||||
export const copyAgencyDataAction = async (postbody) => {
|
||||
const formData = new FormData();
|
||||
Object.keys(postbody).forEach((key) => {
|
||||
formData.append(key, postbody[key]);
|
||||
});
|
||||
const { errcode, result } = await postForm(`${HT_HOST}/Service_BaseInfoWeb/agency_products_copy`, formData);
|
||||
return errcode === 0 ? true : false;
|
||||
};
|
||||
|
||||
export const getAgencyProductsAction = async (param) => {
|
||||
const _param = { ...param, use_year: String(param.use_year || '').replace('all', ''), audit_state: String(param.audit_state || '').replace('all', '') };
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/travel_agency_products`, _param);
|
||||
return errcode !== 0 ? { agency: {}, products: [] } : result;
|
||||
};
|
||||
|
||||
export const getAgencyAllExtrasAction = async (param) => {
|
||||
const _param = { ...param, use_year: String(param.use_year || '').replace('all', ''), };
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_products_extras`, _param);
|
||||
const extrasMapped = result.reduce((acc, curr) => ({...acc, [curr.product_id]: curr.extras}), {});
|
||||
return errcode !== 0 ? {} : extrasMapped;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} body { id, travel_agency_id, extras: [{id, title, code}] }
|
||||
*/
|
||||
export const addProductExtraAction = async (body) => {
|
||||
// console.log('addProductExtraAction', body);
|
||||
// return true; // test: 先不更新到HT
|
||||
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras_add`, body);
|
||||
return errcode === 0 ? true : false;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const delProductExtrasAction = async (body) => {
|
||||
// return true; // test: 先不更新到HT
|
||||
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras_del`, body);
|
||||
return errcode === 0 ? true : false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取指定产品的附加项目
|
||||
* @param {object} param { id, travel_agency_id, use_year }
|
||||
*/
|
||||
export const getAgencyProductExtrasAction = async (param) => {
|
||||
const _param = { ...param, use_year: String(param.use_year || '').replace('all', '') };
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras`, _param);
|
||||
return errcode !== 0 ? [] : result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 审核一条价格
|
||||
*/
|
||||
export const postProductsQuoteAuditAction = async (auditState, quoteRow) => {
|
||||
const postbody = {
|
||||
audit_state: auditState,
|
||||
id: quoteRow.id,
|
||||
travel_agency_id: quoteRow.travel_agency_id,
|
||||
};
|
||||
const formData = new FormData();
|
||||
Object.keys(postbody).forEach((key) => {
|
||||
formData.append(key, postbody[key]);
|
||||
});
|
||||
const json = await postForm(`${HT_HOST}/Service_BaseInfoWeb/quotation_audit`, formData);
|
||||
return json;
|
||||
// return errcode !== 0 ? {} : result;
|
||||
};
|
||||
|
||||
export const postAgencyProductsAuditAction = async (auditState, agency) => {
|
||||
const postbody = {
|
||||
audit_state: auditState,
|
||||
travel_agency_id: agency.travel_agency_id,
|
||||
use_year: agency.use_year,
|
||||
};
|
||||
const formData = new FormData();
|
||||
Object.keys(postbody).forEach((key) => {
|
||||
formData.append(key, postbody[key]);
|
||||
});
|
||||
const json = await postForm(`${HT_HOST}/Service_BaseInfoWeb/agency_products_audit`, formData);
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
* 供应商提交审核
|
||||
*/
|
||||
export const postAgencyAuditAction = async (travel_agency_id, use_year) => {
|
||||
const postbody = {
|
||||
use_year,
|
||||
travel_agency_id,
|
||||
};
|
||||
const formData = new FormData();
|
||||
Object.keys(postbody).forEach((key) => {
|
||||
formData.append(key, postbody[key]);
|
||||
});
|
||||
const { errcode, result } = await postForm(`${HT_HOST}/Service_BaseInfoWeb/agency_submit`, formData);
|
||||
return { errcode, result, success: errcode === 0 };
|
||||
// const { errcode, result } = json;
|
||||
// return errcode !== 0 ? {} : result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存一个产品
|
||||
*/
|
||||
export const postProductsSaveAction = async (products) => {
|
||||
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_save`, products);
|
||||
return { errcode, result, success: errcode === 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除产品报价
|
||||
*/
|
||||
export const deleteQuotationAction = async (id) => {
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_quotation_delete`, {id});
|
||||
return { errcode, result, success: errcode === 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取合同备注
|
||||
*/
|
||||
export const fetchRemarkList = async (params) => {
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_memo_get`, params)
|
||||
return { errcode, result, success: errcode === 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取合同备注
|
||||
*/
|
||||
export const postRemarkList = async (params) => {
|
||||
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_memo_add`, params)
|
||||
return { errcode, result, success: errcode === 0 }
|
||||
}
|
||||
|
||||
const defaultRemarkList = [
|
||||
{id: 0, "product_type_id": "6","Memo": ""},
|
||||
{id: 0, "product_type_id": "B","Memo": ""},
|
||||
{id: 0, "product_type_id": "J","Memo": ""},
|
||||
{id: 0, "product_type_id": "Q","Memo": ""},
|
||||
{id: 0, "product_type_id": "7","Memo": ""},
|
||||
{id: 0, "product_type_id": "R","Memo": ""},
|
||||
{id: 0, "product_type_id": "D","Memo": ""}
|
||||
]
|
||||
|
||||
const initialState = {
|
||||
loading: false,
|
||||
searchValues: {}, // 客服首页: 搜索条件
|
||||
agencyList: [], // 客服首页: 搜索结果
|
||||
activeAgency: {}, // 审核/编辑 页: 当前的供应商
|
||||
activeAgencyState: null,
|
||||
agencyProducts: {}, // 审核/编辑 页: 供应商产品列表
|
||||
editingProduct: {}, // 编辑页: 当前编辑的产品
|
||||
quotationList: [], // 编辑页: 当前产品报价列表
|
||||
editing: false,
|
||||
switchParams: {}, // 头部切换参数
|
||||
}
|
||||
|
||||
export const useProductsStore = create(
|
||||
devtools((set, get) => ({
|
||||
// 初始化状态
|
||||
...initialState,
|
||||
|
||||
// state actions
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setSearchValues: (searchValues) => set({ searchValues }),
|
||||
setAgencyList: (agencyList) => set({ agencyList }),
|
||||
setActiveAgency: (activeAgency) => set({ activeAgency }),
|
||||
setActiveAgencyState: (activeAgencyState) => set({ activeAgencyState }),
|
||||
setAgencyProducts: (agencyProducts) => set({ agencyProducts }),
|
||||
|
||||
setEditingProduct: (product) => {
|
||||
set(() => ({
|
||||
editingProduct: product,
|
||||
quotationList: (product?.quotation??[]).map(q => {
|
||||
return {
|
||||
...q,
|
||||
key: generateId(),
|
||||
fresh: false
|
||||
}
|
||||
})
|
||||
}))
|
||||
},
|
||||
setEditing: (editing) => set({ editing }),
|
||||
setSwitchParams: (switchParams) => set({ switchParams }),
|
||||
|
||||
appendNewProduct: (productItem) => {
|
||||
const { setActiveAgency, agencyProducts, setAgencyProducts } = get();
|
||||
const typeGroup = agencyProducts[productItem.info.product_type_id] || [];
|
||||
const newIndex = typeGroup.findIndex((item) => item.info.id === productItem.info.id);
|
||||
if (newIndex !== -1) {
|
||||
typeGroup.splice(newIndex, 1, productItem);
|
||||
} else {
|
||||
typeGroup.unshift(productItem);
|
||||
}
|
||||
return set({
|
||||
agencyProducts: { ...agencyProducts, [productItem.info.product_type_id]: typeGroup },
|
||||
});
|
||||
},
|
||||
|
||||
reset: () => set(initialState),
|
||||
|
||||
getRemarkList: async() => {
|
||||
const {switchParams} = get()
|
||||
const { result, success } = await fetchRemarkList({
|
||||
travel_agency_id: switchParams.travel_agency_id, use_year: switchParams.use_year
|
||||
})
|
||||
if (success) {
|
||||
const mapRemarkList = defaultRemarkList.map(remark => {
|
||||
const filterResult = result.filter(r => r.product_type_id === remark.product_type_id)
|
||||
if (filterResult.length > 0) return filterResult[0]
|
||||
else return remark
|
||||
})
|
||||
|
||||
return Promise.resolve(mapRemarkList)
|
||||
} else {
|
||||
return Promise.resolve('获取合同备注失败')
|
||||
}
|
||||
},
|
||||
|
||||
saveOrUpdateRemark: async(remarkList) => {
|
||||
const {switchParams} = get()
|
||||
|
||||
const mapRemarkList = remarkList.map(remark => {
|
||||
return {
|
||||
id: remark.id,
|
||||
travel_agency_id: switchParams.travel_agency_id,
|
||||
use_year: switchParams.use_year,
|
||||
product_type_id: remark.product_type_id,
|
||||
Memo: remark.Memo,
|
||||
}
|
||||
})
|
||||
|
||||
const { result, success } = await postRemarkList(mapRemarkList)
|
||||
if (success) {
|
||||
return Promise.resolve(result)
|
||||
} else {
|
||||
return Promise.resolve('保存合同备注失败')
|
||||
}
|
||||
},
|
||||
|
||||
newEmptyQuotation: () => ({
|
||||
id: null,
|
||||
adult_cost: 0,
|
||||
child_cost: 0,
|
||||
currency: 'RMB',
|
||||
unit_id: '0',
|
||||
group_size_min: 1,
|
||||
group_size_max: 10,
|
||||
use_dates: [
|
||||
dayjs().startOf('M'),
|
||||
dayjs().endOf('M')
|
||||
],
|
||||
weekdayList: [],
|
||||
fresh: true // 标识是否是新记录,新记录才用添加列表
|
||||
}),
|
||||
|
||||
appendQuotationList: (defList) => {
|
||||
const { activeAgency, editingProduct, quotationList } = get()
|
||||
const generatedList = []
|
||||
|
||||
defList.forEach(definition => {
|
||||
definition?.useDateList.map(useDateItem => {
|
||||
const mappedPriceList = definition?.priceList.map(price => {
|
||||
return {
|
||||
id: null,
|
||||
adult_cost: price.priceInput.audultPrice,
|
||||
child_cost: price.priceInput.childrenPrice,
|
||||
group_size_min: price.priceInput.numberStart,
|
||||
group_size_max: price.priceInput.numberEnd,
|
||||
|
||||
currency: definition.currency,
|
||||
unit_id: definition.unitId,
|
||||
// 保持和 API 返回格式一致,日期要转换为字符串
|
||||
use_dates_start: useDateItem.useDate[0].format('YYYY-MM-DD'),
|
||||
use_dates_end: useDateItem.useDate[1].format('YYYY-MM-DD'),
|
||||
weekdays: definition.weekend.join(','),
|
||||
WPI_SN: editingProduct.info.id,
|
||||
WPP_VEI_SN: activeAgency.travel_agency_id,
|
||||
lastedit_changed: '',
|
||||
audit_state_id: -1,
|
||||
key: generateId(),
|
||||
fresh: false
|
||||
}
|
||||
})
|
||||
generatedList.push(...mappedPriceList)
|
||||
})
|
||||
})
|
||||
|
||||
const mergedList = [...quotationList,...generatedList]
|
||||
set(() => ({
|
||||
quotationList: mergedList
|
||||
}))
|
||||
|
||||
return mergedList
|
||||
},
|
||||
|
||||
saveOrUpdateQuotation: (formValues) => {
|
||||
|
||||
const { activeAgency, editingProduct, quotationList } = get()
|
||||
let mergedList = []
|
||||
|
||||
formValues.WPI_SN = editingProduct.info.id
|
||||
formValues.WPP_VEI_SN = activeAgency.travel_agency_id
|
||||
formValues.use_dates_start = formValues.use_dates[0].format('YYYY-MM-DD')
|
||||
formValues.use_dates_end = formValues.use_dates[1].format('YYYY-MM-DD')
|
||||
formValues.weekdays = formValues.weekdayList.join(',')
|
||||
|
||||
if (formValues.fresh) {
|
||||
formValues.key = generateId()
|
||||
formValues.lastedit_changed = ''
|
||||
formValues.audit_state_id = -1 // 新增,
|
||||
formValues.fresh = false // 添加到列表后就不是新纪录,保存要修改原来记录
|
||||
mergedList = [...quotationList,...[formValues]]
|
||||
} else {
|
||||
mergedList = quotationList.map(prevQuotation => {
|
||||
if (prevQuotation.key === formValues.key) {
|
||||
const changedList = []
|
||||
for (const [key, value] of Object.entries(formValues)) {
|
||||
if (key === 'use_dates' || key === 'id' || key === 'key') continue
|
||||
|
||||
const preValue = prevQuotation[key]
|
||||
const hasChanged = preValue !== value
|
||||
|
||||
if (hasChanged) {
|
||||
changedList.push({
|
||||
[key]: preValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...prevQuotation,
|
||||
audit_state_id: -1,
|
||||
adult_cost: formValues.adult_cost,
|
||||
child_cost: formValues.child_cost,
|
||||
currency: formValues.currency,
|
||||
unit_id: formValues.unit_id,
|
||||
group_size_min: formValues.group_size_min,
|
||||
group_size_max: formValues.group_size_max,
|
||||
use_dates_start: formValues.use_dates_start,
|
||||
use_dates_end: formValues.use_dates_end,
|
||||
weekdays: formValues.weekdays,
|
||||
lastedit_changed: JSON.stringify(changedList, null, 2)
|
||||
}
|
||||
} else {
|
||||
return prevQuotation
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
set(() => ({
|
||||
quotationList: mergedList
|
||||
}))
|
||||
|
||||
return mergedList
|
||||
},
|
||||
|
||||
deleteQuotation: async(quotation) => {
|
||||
const { editingProduct, quotationList, agencyProducts } = get()
|
||||
const productTypeId = editingProduct.info.product_type_id;
|
||||
const quotationId = quotation.id
|
||||
const newQuotationList = quotationList.filter(q => {
|
||||
return q.key != quotation.key
|
||||
})
|
||||
|
||||
const newProductList = agencyProducts[productTypeId].map(p => {
|
||||
if (p.info.id == editingProduct.info.id) {
|
||||
return {
|
||||
...editingProduct,
|
||||
quotation: newQuotationList
|
||||
}
|
||||
} else {
|
||||
return p
|
||||
}
|
||||
})
|
||||
|
||||
set({
|
||||
agencyProducts: {
|
||||
...agencyProducts,
|
||||
[productTypeId]: newProductList
|
||||
},
|
||||
quotationList: newQuotationList
|
||||
})
|
||||
|
||||
let promiseDelete = Promise.resolve(newQuotationList)
|
||||
|
||||
if (isNotEmpty(quotationId)) {
|
||||
const { result, success } = await deleteQuotationAction(quotationId)
|
||||
if (!success) {
|
||||
promiseDelete = Promise.reject(result)
|
||||
}
|
||||
}
|
||||
|
||||
return promiseDelete
|
||||
},
|
||||
|
||||
// side effects
|
||||
searchAgency: async (param) => {
|
||||
const { setLoading, setAgencyList } = get();
|
||||
setLoading(true);
|
||||
const res = await searchAgencyAction(param);
|
||||
setAgencyList(res);
|
||||
setLoading(false);
|
||||
},
|
||||
|
||||
getAgencyProducts: async (param) => {
|
||||
const { setLoading, setActiveAgency, setActiveAgencyState, setAgencyProducts, editingProduct, setEditingProduct } = get();
|
||||
setLoading(true);
|
||||
setAgencyProducts({});
|
||||
// setEditingProduct({});
|
||||
const res = await getAgencyProductsAction(param);
|
||||
|
||||
const productsData = groupBy(res.products, (row) => row.info.product_type_id);
|
||||
setAgencyProducts(productsData);
|
||||
setActiveAgency(res.agency);
|
||||
setActiveAgencyState(res.agency.audit_state_id);
|
||||
if (editingProduct?.info?.id) {
|
||||
const item = (productsData[editingProduct.info.product_type_id] || []).find((item) => item.info.id === editingProduct.info.id);
|
||||
setEditingProduct(item);
|
||||
} else {
|
||||
setEditingProduct({});
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
|
||||
getAgencyProductExtras: async (param) => {
|
||||
const res = await getAgencyProductExtrasAction(param);
|
||||
// todo:
|
||||
},
|
||||
}), { name: 'productStore' })
|
||||
);
|
||||
export default useProductsStore;
|
@ -1,113 +1,69 @@
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
import { fetchJSON, postForm } from "@/utils/request";
|
||||
import { prepareUrl, isNotEmpty } from "@/utils/commons";
|
||||
import { fetchJSON } from "@/utils/request";
|
||||
import { HT_HOST } from "@/config";
|
||||
import { json } from "react-router-dom";
|
||||
import * as config from "@/config";
|
||||
import dayjs from "dayjs";
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
|
||||
class Report {
|
||||
constructor(root) {
|
||||
makeAutoObservable(this, { rootStore: false });
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
vendorScoresData = []; //地接统计数据集,合计数据,每月数据,地接考核分数
|
||||
productScoresData = []; //产品体验分析 常用酒店分析, 导游接待情况
|
||||
commendScoresData = []; //表扬情况, 投诉情况, 评建议
|
||||
|
||||
loading = false;
|
||||
search_date_start = dayjs().month(0).startOf("month");
|
||||
search_date_end = dayjs().month(11).endOf("month");
|
||||
|
||||
onDateRangeChange = dates => {
|
||||
this.search_date_start = dates == null ? null : dates[0].startOf("month");
|
||||
this.search_date_end = dates == null ? null : dates[1].endOf("month");
|
||||
const initialState = {
|
||||
loading: false,
|
||||
vendorScoresData: [], //地接统计数据集,合计数据,每月数据,地接考核分数
|
||||
productScoresData: [], //产品体验分析 常用酒店分析, 导游接待情况
|
||||
commendScoresData: [], //表扬情况, 投诉情况, 评建议
|
||||
};
|
||||
export const useReportStore = create(
|
||||
devtools((set, get) => ({
|
||||
...initialState,
|
||||
reset: () => set(initialState),
|
||||
|
||||
getHWVendorScores(VEI_SN, StartDate, EndDate) {
|
||||
this.loading = true;
|
||||
const fetchUrl = prepareUrl(HT_HOST +"/service-cusservice/PTGetHWVendorScores")
|
||||
.append("VEI_SN", VEI_SN)
|
||||
.append("StartDate", StartDate)
|
||||
.append("EndDate", EndDate)
|
||||
.append("StrDEI_SN", "(,-1,)")
|
||||
.append("OrderType", "-1")
|
||||
.append("GroupType", "-1")
|
||||
.append("token", this.root.authStore.login.token)
|
||||
.build();
|
||||
|
||||
return fetchJSON(fetchUrl).then(json => {
|
||||
runInAction(() => {
|
||||
this.loading = false;
|
||||
if (json.errcode == 0) {
|
||||
if (isNotEmpty(json)) {
|
||||
this.vendorScoresData = json;
|
||||
} else {
|
||||
this.vendorScoresData = [];
|
||||
}
|
||||
} else {
|
||||
throw new Error(json.errmsg + ": " + json.errcode);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getHWProductScores(VEI_SN, StartDate, EndDate) {
|
||||
this.loading = true;
|
||||
const fetchUrl = prepareUrl(HT_HOST +"/service-cusservice/PTGetHWProductScores")
|
||||
.append("VEI_SN", VEI_SN)
|
||||
.append("StartDate", StartDate)
|
||||
.append("EndDate", EndDate)
|
||||
.append("StrDEI_SN", "(,-1,)")
|
||||
.append("OrderType", "-1")
|
||||
.append("GroupType", "-1")
|
||||
.append("token", this.root.authStore.login.token)
|
||||
.build();
|
||||
|
||||
return fetchJSON(fetchUrl).then(json => {
|
||||
runInAction(() => {
|
||||
this.loading = false;
|
||||
if (json.errcode == 0) {
|
||||
if (isNotEmpty(json)) {
|
||||
this.productScoresData = json;
|
||||
} else {
|
||||
this.productScoresData = [];
|
||||
}
|
||||
} else {
|
||||
throw new Error(json.errmsg + ": " + json.errcode);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setVendorScoresData: (vendorScoresData) => set({ vendorScoresData }),
|
||||
setProductScoresData: (productScoresData) => set({ productScoresData }),
|
||||
setCommendScoresData: (commendScoresData) => set({ commendScoresData }),
|
||||
|
||||
getHWCommendScores(VEI_SN, StartDate, EndDate) {
|
||||
this.loading = true;
|
||||
const fetchUrl = prepareUrl(HT_HOST +"/service-cusservice/PTGetHWCommendScores")
|
||||
.append("VEI_SN", VEI_SN)
|
||||
.append("StartDate", StartDate)
|
||||
.append("EndDate", EndDate)
|
||||
.append("StrDEI_SN", "(,-1,)")
|
||||
.append("OrderType", "-1")
|
||||
.append("GroupType", "-1")
|
||||
.append("token", this.root.authStore.login.token)
|
||||
.build();
|
||||
|
||||
return fetchJSON(fetchUrl).then(json => {
|
||||
runInAction(() => {
|
||||
this.loading = false;
|
||||
if (json.errcode == 0) {
|
||||
if (isNotEmpty(json)) {
|
||||
this.commendScoresData = json;
|
||||
} else {
|
||||
this.commendScoresData = [];
|
||||
}
|
||||
} else {
|
||||
throw new Error(json.errmsg + ": " + json.errcode);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Report;
|
||||
async getHWVendorScores(VEI_SN, StartDate, EndDate) {
|
||||
const { setLoading, setVendorScoresData } = get();
|
||||
setLoading(true);
|
||||
const searchParams = {
|
||||
VEI_SN,
|
||||
StartDate,
|
||||
EndDate,
|
||||
StrDEI_SN: '(,-1,)',
|
||||
OrderType: '-1',
|
||||
GroupType: '-1',
|
||||
};
|
||||
const { errcode, ...Result } = await fetchJSON(`${HT_HOST}/service-cusservice/PTGetHWVendorScores`, searchParams);
|
||||
setVendorScoresData(errcode === 0 ? Result : {});
|
||||
// setLoading(false);
|
||||
},
|
||||
async getHWProductScores(VEI_SN, StartDate, EndDate) {
|
||||
const { setLoading, setProductScoresData } = get();
|
||||
setLoading(true);
|
||||
const searchParams = {
|
||||
VEI_SN,
|
||||
StartDate,
|
||||
EndDate,
|
||||
StrDEI_SN: '(,-1,)',
|
||||
OrderType: '-1',
|
||||
GroupType: '-1',
|
||||
};
|
||||
const { errcode, ...Result } = await fetchJSON(`${HT_HOST}/service-cusservice/PTGetHWProductScores`, searchParams);
|
||||
setProductScoresData(errcode === 0 ? Result : {});
|
||||
setLoading(false);
|
||||
},
|
||||
async getHWCommendScores(VEI_SN, StartDate, EndDate) {
|
||||
const { setLoading, setCommendScoresData } = get();
|
||||
setLoading(true);
|
||||
const searchParams = {
|
||||
VEI_SN,
|
||||
StartDate,
|
||||
EndDate,
|
||||
StrDEI_SN: '(,-1,)',
|
||||
OrderType: '-1',
|
||||
GroupType: '-1',
|
||||
};
|
||||
const { errcode, ...Result } = await fetchJSON(`${HT_HOST}/service-cusservice/PTGetHWCommendScores`, searchParams);
|
||||
setCommendScoresData(errcode === 0 ? Result : {});
|
||||
// setLoading(false);
|
||||
},
|
||||
}), { name: 'reportStore'})
|
||||
);
|
||||
export default useReportStore;
|
||||
|
@ -1,49 +0,0 @@
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import Reservation from "./Reservation";
|
||||
import Feedback from "./Feedback";
|
||||
import Notice from "./Notice";
|
||||
import Auth from "./Auth";
|
||||
import Invoice from "./Invoice";
|
||||
import Report from "./Report";
|
||||
|
||||
class Root {
|
||||
constructor() {
|
||||
this.reservationStore = new Reservation(this);
|
||||
this.feedbackStore = new Feedback(this);
|
||||
this.noticeStore = new Notice(this);
|
||||
this.authStore = new Auth(this);
|
||||
this.invoiceStore = new Invoice(this);
|
||||
this.reportStore = new Report(this);
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
clearSession() {
|
||||
if (window.sessionStorage) {
|
||||
const sessionStorage = window.sessionStorage;
|
||||
sessionStorage.clear();
|
||||
} else {
|
||||
console.error('browser not support sessionStorage!');
|
||||
}
|
||||
}
|
||||
|
||||
getSession(key) {
|
||||
if (window.sessionStorage) {
|
||||
const sessionStorage = window.sessionStorage;
|
||||
return sessionStorage.getItem(key);
|
||||
} else {
|
||||
console.error('browser not support sessionStorage!');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
putSession(key, value) {
|
||||
if (window.sessionStorage) {
|
||||
const sessionStorage = window.sessionStorage;
|
||||
return sessionStorage.setItem(key, value);
|
||||
} else {
|
||||
console.error('browser not support sessionStorage!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Root;
|
@ -1,7 +0,0 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
export const StoreContext = createContext();
|
||||
|
||||
export function useStore() {
|
||||
return useContext(StoreContext);
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
export const ThemeContext = createContext({})
|
||||
|
||||
export function useThemeContext() {
|
||||
return useContext(ThemeContext)
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
const initListener = []
|
||||
const authListener = []
|
||||
|
||||
export const addInitLinstener = (fn) => {
|
||||
initListener.push(fn)
|
||||
}
|
||||
|
||||
export const addAuthLinstener = (fn) => {
|
||||
authListener.push(fn)
|
||||
}
|
||||
|
||||
export const notifyInit = async () => {
|
||||
for (const listener of initListener) {
|
||||
await listener()
|
||||
}
|
||||
}
|
||||
|
||||
export const notifyAuth = async (obj) => {
|
||||
for (const listener of authListener) {
|
||||
await listener(obj)
|
||||
}
|
||||
}
|
||||
|
||||
// Zustand 中间件,用于订阅前端应用的生命周期,实验阶段。
|
||||
// 失败,无法同步调用异步方法!
|
||||
export const lifecycleware = (fn) => (set, get, store) => {
|
||||
|
||||
addInitLinstener(() => {
|
||||
if (store.getState().hasOwnProperty('onInit')) {
|
||||
store.getState().onInit()
|
||||
} else {
|
||||
console.info('store has no function: onInit.')
|
||||
}
|
||||
})
|
||||
|
||||
addAuthLinstener(() => {
|
||||
if (store.getState().hasOwnProperty('onAuth')) {
|
||||
store.getState().onAuth()
|
||||
} else {
|
||||
console.info('store has no function: onAuth.')
|
||||
}
|
||||
})
|
||||
|
||||
return fn(set, get, store)
|
||||
}
|
@ -1,69 +1,154 @@
|
||||
|
||||
import { BUILD_VERSION } from '@/config'
|
||||
|
||||
const customHeaders = []
|
||||
|
||||
// 添加 HTTP Reuqest 自定义头部
|
||||
export function appendRequestHeader(n, v) {
|
||||
customHeaders.push({
|
||||
name: n,
|
||||
value: v
|
||||
})
|
||||
}
|
||||
|
||||
function getRequestHeader() {
|
||||
return customHeaders.reduce((acc, item) => {
|
||||
acc[item.name] = item.value;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
const initParams = [];
|
||||
export function appendRequestParams(n, v) {
|
||||
initParams.push({
|
||||
name: n,
|
||||
value: v
|
||||
})
|
||||
}
|
||||
function getRequestInitParams() {
|
||||
return initParams.reduce((acc, item) => {
|
||||
acc[item.name] = item.value;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function checkStatus(response) {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response;
|
||||
return response
|
||||
} else {
|
||||
const message =
|
||||
'Fetch error: ' + response.url + ' ' + response.status + ' (' +
|
||||
response.statusText + ')';
|
||||
const error = new Error(message);
|
||||
error.response = response;
|
||||
throw error;
|
||||
response.statusText + ')'
|
||||
const error = new Error(message)
|
||||
error.response = response
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function checkBizCode(responseJson) {
|
||||
if (responseJson.errcode === 0) {
|
||||
return responseJson;
|
||||
} else {
|
||||
throw new Error(responseJson.errmsg + ': ' + responseJson.errcode);
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchText(url) {
|
||||
return fetch(url)
|
||||
.then(checkStatus)
|
||||
const headerObj = getRequestHeader()
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Web-Version': BUILD_VERSION,
|
||||
...headerObj
|
||||
}
|
||||
}).then(checkStatus)
|
||||
.then(response => response.text())
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchJSON(url) {
|
||||
return fetch(url)
|
||||
.then(checkStatus)
|
||||
export function fetchJSON(url, data = {}) {
|
||||
const initParams = getRequestInitParams();
|
||||
const params4get = Object.assign({}, initParams, data);
|
||||
const params = params4get ? new URLSearchParams(params4get).toString() : '';
|
||||
const ifp = url.includes('?') ? '&' : '?';
|
||||
const headerObj = getRequestHeader();
|
||||
const fUrl = params !== '' ? `${url}${ifp}${params}` : url;
|
||||
return fetch(fUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Web-Version': BUILD_VERSION,
|
||||
...headerObj
|
||||
}
|
||||
}).then(checkStatus)
|
||||
.then(response => response.json())
|
||||
.then(checkBizCode)
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
export function postForm(url, data) {
|
||||
const initParams = getRequestInitParams();
|
||||
Object.keys(initParams).forEach(key => {
|
||||
if (! data.has(key)) {
|
||||
data.append(key, initParams[key]);
|
||||
}
|
||||
});
|
||||
const headerObj = getRequestHeader()
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
body: data
|
||||
body: data,
|
||||
headers: {
|
||||
'X-Web-Version': BUILD_VERSION,
|
||||
...headerObj
|
||||
}
|
||||
}).then(checkStatus)
|
||||
.then(response => response.json())
|
||||
.then(checkBizCode)
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
export function postJSON(url, obj) {
|
||||
return fetch(url, {
|
||||
const initParams = getRequestInitParams();
|
||||
const params4get = Object.assign({}, initParams);
|
||||
const params = new URLSearchParams(params4get).toString();
|
||||
const ifp = url.includes('?') ? '&' : '?';
|
||||
const fUrl = params !== '' ? `${url}${ifp}${params}` : url;
|
||||
|
||||
const headerObj = getRequestHeader()
|
||||
return fetch(fUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(obj),
|
||||
headers: {
|
||||
'Content-type': 'application/json; charset=UTF-8'
|
||||
'Content-type': 'application/json; charset=UTF-8',
|
||||
'X-Web-Version': BUILD_VERSION,
|
||||
...headerObj
|
||||
}
|
||||
}).then(checkStatus)
|
||||
.then(response => response.json())
|
||||
.then(checkBizCode)
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
export function postStream(url, obj) {
|
||||
const headerObj = getRequestHeader()
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(obj),
|
||||
headers: {
|
||||
'Content-type': 'application/octet-stream'
|
||||
'Content-type': 'application/octet-stream',
|
||||
'X-Web-Version': BUILD_VERSION,
|
||||
...headerObj
|
||||
}
|
||||
}).then(checkStatus)
|
||||
.then(response => response.json())
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
export default function Index() {
|
||||
return (
|
||||
<p id="zero-state">
|
||||
Global Highlights Hub
|
||||
<br />
|
||||
Check out{" "}
|
||||
<a href="https://www.chinahighlights.com">
|
||||
the docs at chinahighlights.com
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
);
|
||||
}
|
@ -1,119 +1,109 @@
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useEffect } from 'react';
|
||||
import { Button, Checkbox, Form, Input, Row, App } from 'antd';
|
||||
import { useStore } from '@/stores/StoreContext.js';
|
||||
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useEffect } from 'react'
|
||||
import { Button, Form, Input, Row, Radio, App } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useAuthStore from '@/stores/Auth'
|
||||
import { appendRequestParams } from '@/utils/request'
|
||||
|
||||
function Login() {
|
||||
const [authenticate, loginStatus, defaultRoute] =
|
||||
useAuthStore((state) => [state.authenticate, state.loginStatus, state.defaultRoute])
|
||||
|
||||
const { t, i18n } = useTranslation()
|
||||
const { notification } = App.useApp()
|
||||
const navigate = useNavigate()
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const { authStore, noticeStore } = useStore();
|
||||
const { notification } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [form] = Form.useForm();
|
||||
const handleLngChange = (lng) => {
|
||||
appendRequestParams('lgc', lng === 'zh' ? 2 : 1)
|
||||
i18n.changeLanguage(lng)
|
||||
}
|
||||
|
||||
const defaultLng = i18n.language??'zh'
|
||||
appendRequestParams('lgc', defaultLng === 'zh' ? 2 : 1)
|
||||
|
||||
useEffect (() => {
|
||||
if (location.search === '?out') {
|
||||
authStore.logout();
|
||||
navigate('/login');
|
||||
if (loginStatus === 302) {
|
||||
navigate(defaultRoute)
|
||||
}
|
||||
return () => {
|
||||
// unmount...
|
||||
};
|
||||
}, []);
|
||||
}, [loginStatus])
|
||||
|
||||
const onFinish = (values) => {
|
||||
authStore.valdateUserPassword(values.username, values.password)
|
||||
.then((userId) => {
|
||||
noticeStore.getBulletinUnReadCount(userId);
|
||||
authStore.fetchUserDetail()
|
||||
.then((user) => {
|
||||
// navigate(-1) is equivalent to hitting the back button.
|
||||
navigate("/reservation/newest");
|
||||
})
|
||||
authenticate(values.username, values.password)
|
||||
.catch(ex => {
|
||||
console.error(ex)
|
||||
notification.error({
|
||||
message: `Notification`,
|
||||
description: 'Failed to get user information.',
|
||||
message: t('Validation.Title'),
|
||||
description: t('Validation.LoginFailed'),
|
||||
placement: 'top',
|
||||
duration: 4,
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(ex => {
|
||||
notification.error({
|
||||
message: `Notification`,
|
||||
description: 'Login failed. Incorrect username or password.',
|
||||
placement: 'top',
|
||||
duration: 4,
|
||||
});
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const onFinishFailed = (errorInfo) => {
|
||||
console.log('Failed:', errorInfo);
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Row justify="center" align="middle" style={{ minHeight: 500 }}>
|
||||
<Row justify='center' align='middle' className='min-h-96'>
|
||||
<Form
|
||||
name="basic"
|
||||
// layout="vertical"
|
||||
name='login'
|
||||
layout='vertical'
|
||||
form={form}
|
||||
size="large"
|
||||
size='large'
|
||||
labelCol={{
|
||||
span: 8,
|
||||
}}
|
||||
wrapperCol={{
|
||||
span: 16,
|
||||
}}
|
||||
style={{
|
||||
maxWidth: 600,
|
||||
span: 24,
|
||||
}}
|
||||
className='max-w-xl'
|
||||
initialValues={{
|
||||
remember: true,
|
||||
language: defaultLng,
|
||||
}}
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={onFinishFailed}
|
||||
autoComplete="off"
|
||||
autoComplete='off'
|
||||
>
|
||||
<Form.Item
|
||||
label="Username"
|
||||
name="username"
|
||||
label={t('Username')}
|
||||
name='username'
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please input your username!',
|
||||
message: t('Validation.UsernameIsEmpty'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Password"
|
||||
name="password"
|
||||
label={t('Password')}
|
||||
name='password'
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please input your password!',
|
||||
message: t('Validation.PasswordIsEmpty'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
wrapperCol={{
|
||||
offset: 8,
|
||||
span: 16,
|
||||
}}
|
||||
>
|
||||
<Button type="primary" htmlType="submit" style={{width: "100%"}}>
|
||||
Login
|
||||
<Form.Item name='language'>
|
||||
<Radio.Group onChange={e => handleLngChange(e.target.value)}>
|
||||
<Radio value='zh'>中文</Radio>
|
||||
<Radio value='en'>English</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type='primary' htmlType='submit' className='w-full'>
|
||||
{t('Login')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Row>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default Login;
|
||||
export default Login
|
||||
|
@ -0,0 +1,31 @@
|
||||
import { Flex, Result, Spin } from 'antd'
|
||||
import { useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import useAuthStore from '@/stores/Auth'
|
||||
|
||||
function Logout() {
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
const logout = useAuthStore(state => state.logout)
|
||||
|
||||
useEffect(() => {
|
||||
logout()
|
||||
navigate('/login')
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Flex justify='center' align='center' gap='middle' vertical>
|
||||
<Result
|
||||
status='success'
|
||||
title='退出成功'
|
||||
subTitle='正在跳转登陆页面'
|
||||
extra={[
|
||||
<Spin key='small-span' size='small' />
|
||||
]}
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
export default Logout
|
@ -1,57 +1,37 @@
|
||||
import { Outlet, Link, useHref, useLocation } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Layout, Menu, ConfigProvider, theme, Typography, Space, Row, Col, Alert, App as AntApp } from "antd";
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Layout, ConfigProvider, theme, Row, Col, App as AntApp } from "antd";
|
||||
import "antd/dist/reset.css";
|
||||
import AppLogo from "@/assets/logo-gh.png";
|
||||
import { useStore } from "@/stores/StoreContext.js";
|
||||
import { useThemeContext } from "@/stores/ThemeContext";
|
||||
import { BUILD_VERSION } from "@/config";
|
||||
|
||||
const { Title } = Typography;
|
||||
const { Header, Content, Footer } = Layout;
|
||||
|
||||
function Standlone() {
|
||||
const { authStore } = useStore();
|
||||
|
||||
const {
|
||||
token: { colorBgContainer },
|
||||
} = theme.useToken();
|
||||
const { colorPrimary } = useThemeContext();
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: "#00b96b",
|
||||
colorPrimary: colorPrimary,
|
||||
},
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
}}>
|
||||
<AntApp>
|
||||
<Layout
|
||||
style={{
|
||||
minHeight: "100vh",
|
||||
}}>
|
||||
<Header className="header" style={{ position: "sticky", top: 0, zIndex: 1, width: "100%" }}>
|
||||
<Row gutter={{ md: 24 }} justify="center">
|
||||
<Col span={4}>
|
||||
<img src={AppLogo} className="logo" alt="App logo" />
|
||||
</Col>
|
||||
<Col span={20}><Title style={{ color: "white", marginTop: "3.5px" }}>Global Highlights Hub</Title></Col>
|
||||
</Row>
|
||||
<Layout className="min-h-screen">
|
||||
<Header className="sticky top-0 z-10 w-full">
|
||||
<img src={AppLogo} className="float-left h-9 my-4 mr-6 ml-0 bg-white/30" alt="App logo" />
|
||||
<p className="text-white text-center">Global Highlights Hub</p>
|
||||
</Header>
|
||||
<Content
|
||||
style={{
|
||||
padding: 24,
|
||||
margin: 0,
|
||||
minHeight: 280,
|
||||
background: colorBgContainer,
|
||||
}}>
|
||||
<Content className="p-6 m-0 min-h-72 bg-white">
|
||||
<Outlet />
|
||||
</Content>
|
||||
<Footer></Footer>
|
||||
<Footer>China Highlights International Travel Service Co., LTD, Version: {BUILD_VERSION}</Footer>
|
||||
</Layout>
|
||||
</AntApp>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Standlone);
|
||||
export default Standlone;
|
||||
|
@ -1,113 +0,0 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Button, Space, Form, Input, Row, Typography, App,Select } from "antd";
|
||||
import { useStore } from "@/stores/StoreContext.js";
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
function ChangeVendor() {
|
||||
const navigate = useNavigate();
|
||||
const { authStore,VendorList } = useStore();
|
||||
const { notification } = App.useApp();
|
||||
const [form] = Form.useForm();
|
||||
const { formVeiSn, onVeiSnChange } = useState();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
authStore.fetchVendorList();
|
||||
},[]);
|
||||
|
||||
//币种
|
||||
function bindVendor() {
|
||||
|
||||
let arr=[];
|
||||
arr = authStore.VendorList.map((data,index) =>{
|
||||
return {
|
||||
value: data.VEI_SN,
|
||||
label: data.VEI2_CompanyBN,
|
||||
}
|
||||
})
|
||||
return arr;
|
||||
|
||||
}
|
||||
|
||||
const onFinish = (values) => {
|
||||
if (values.VEISN == authStore.login.travelAgencyId){
|
||||
notification.error({
|
||||
message: `Notification`,
|
||||
description: "切换的供应商是当前供应商.",
|
||||
placement: "top",
|
||||
duration: 4,
|
||||
});
|
||||
return ;
|
||||
}
|
||||
// console.log(values);
|
||||
// console.log(authStore.login.travelAgencyId);
|
||||
|
||||
authStore.changeVendor(values.VEISN)
|
||||
.then(()=>{
|
||||
authStore.logout();
|
||||
})
|
||||
.catch(()=>{
|
||||
//console.log(json);
|
||||
notification.error({
|
||||
message: `Notification`,
|
||||
description: "切换的供应商错误,请重试!",
|
||||
placement: "top",
|
||||
duration: 4,
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
const onFinishFailed = (errorInfo) => {
|
||||
console.log("Failed:", errorInfo);
|
||||
// form.resetFields();
|
||||
};
|
||||
|
||||
return (
|
||||
<Row justify="center" align="middle" style={{ minHeight: 500 }}>
|
||||
<Form
|
||||
name="basic"
|
||||
form={form}
|
||||
layout="vertical"
|
||||
size="large"
|
||||
style={{
|
||||
maxWidth: 600,
|
||||
}}
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={onFinishFailed}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item>
|
||||
<Title level={2}>Change your password</Title>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="VEISN"
|
||||
label="供应商"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请选择需要切换的供应商!",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择"
|
||||
onChange={onVeiSnChange}
|
||||
options={bindVendor()}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space size="middle">
|
||||
<Button type="primary" htmlType="submit">
|
||||
选 择
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(ChangeVendor);
|
@ -1,23 +1,24 @@
|
||||
import { Descriptions, Col, Row } from 'antd';
|
||||
import { useStore } from '@/stores/StoreContext.js';
|
||||
import { Descriptions, Col, Row } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useAuthStore from '@/stores/Auth'
|
||||
|
||||
function Profile() {
|
||||
|
||||
const { authStore } = useStore();
|
||||
const { login } = authStore;
|
||||
const { t } = useTranslation()
|
||||
const currentUser = useAuthStore(state => state.currentUser)
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col span={12} offset={6}>
|
||||
<Descriptions title="User Profile" layout="vertical" column={2}>
|
||||
<Descriptions.Item label="Username">{login.username}</Descriptions.Item>
|
||||
<Descriptions.Item label="Telephone">{login.telephone}</Descriptions.Item>
|
||||
<Descriptions.Item label="Email address">{login.emailAddress}</Descriptions.Item>
|
||||
<Descriptions.Item label="Company">{login.travelAgencyName}</Descriptions.Item>
|
||||
<Descriptions title={t('userProfile')} layout="vertical" column={2}>
|
||||
<Descriptions.Item label={t("Username")}>{currentUser?.username}</Descriptions.Item>
|
||||
<Descriptions.Item label={t("Realname")}>{currentUser?.realname}</Descriptions.Item>
|
||||
<Descriptions.Item label={t("Email")}>{currentUser?.emailAddress}</Descriptions.Item>
|
||||
<Descriptions.Item label={t("Company")}>{currentUser?.travelAgencyName}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default Profile;
|
||||
export default Profile
|
||||
|
@ -0,0 +1,139 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Grid, Divider, Layout, Spin, Input, Col, Row, Space, List, Table, Button } from "antd";
|
||||
import { PhoneOutlined, CustomerServiceOutlined, AudioOutlined, AuditOutlined } from "@ant-design/icons";
|
||||
import { useParams, useHref, useNavigate, NavLink } from "react-router-dom";
|
||||
import { isEmpty, formatColonTime } from "@/utils/commons";
|
||||
import dayjs from "dayjs";
|
||||
import SearchForm from "@/components/SearchForm";
|
||||
import { DATE_FORMAT } from "@/config";
|
||||
import { TableExportBtn } from "@/components/Data";
|
||||
import airTicketStore from "@/stores/Airticket";
|
||||
import { usingStorage } from "@/hooks/usingStorage";
|
||||
|
||||
const planListColumns = [
|
||||
{
|
||||
title: "团名",
|
||||
key: "GRI_No",
|
||||
dataIndex: "GRI_No",
|
||||
// sorter: (a, b) => b.GRI_No - a.GRI_No,
|
||||
},
|
||||
{
|
||||
title: "组团人",
|
||||
key: "WL",
|
||||
dataIndex: "WL",
|
||||
},
|
||||
{
|
||||
title: "人数",
|
||||
dataIndex: "PersonNum",
|
||||
key: "PersonNum",
|
||||
},
|
||||
{
|
||||
title: "出发日期",
|
||||
key: "StartDate",
|
||||
dataIndex: "StartDate",
|
||||
sorter: (a, b) => {
|
||||
const dateA = new Date(a.StartDate);
|
||||
const dateB = new Date(b.StartDate);
|
||||
return dateB.getTime() - dateA.getTime();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "出发城市",
|
||||
key: "FromCity",
|
||||
dataIndex: "FromCity",
|
||||
},
|
||||
{
|
||||
title: "抵达城市",
|
||||
key: "ToCity",
|
||||
dataIndex: "ToCity",
|
||||
},
|
||||
{
|
||||
title: "航班",
|
||||
key: "FlightNo",
|
||||
dataIndex: "FlightNo",
|
||||
},
|
||||
{
|
||||
title: "起飞时间",
|
||||
key: "FlightStart",
|
||||
dataIndex: "FlightStart",
|
||||
render: text => formatColonTime(text),
|
||||
},
|
||||
{
|
||||
title: "落地时间",
|
||||
key: "FlightEnd",
|
||||
dataIndex: "FlightEnd",
|
||||
render: text => formatColonTime(text),
|
||||
},
|
||||
{
|
||||
title: "出票处理",
|
||||
key: "TicketIssued",
|
||||
dataIndex: "TicketIssued",
|
||||
render: (text, record) => record.TicketIssuedName,
|
||||
},
|
||||
{
|
||||
title: "计划状态",
|
||||
key: "FlightStatus",
|
||||
dataIndex: "FlightStatus",
|
||||
render: (text, record) => record.FlightStatusName,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "FlightInfo",
|
||||
dataIndex: "FlightInfo",
|
||||
render: (text, record) => <NavLink to={`/airticket/plan/${record.COLI_SN}/${record.GRI_SN}`}>{"编辑"}</NavLink>,
|
||||
},
|
||||
];
|
||||
|
||||
const Airticket = props => {
|
||||
const navigate = useNavigate();
|
||||
const { travelAgencyId } = usingStorage();
|
||||
const [getPlanList, planList, loading] = airTicketStore(state => [state.getPlanList, state.planList, state.loading]);
|
||||
const showTotal = total => `合计 ${total} `;
|
||||
|
||||
useEffect(() => {
|
||||
!planList && getPlanList(travelAgencyId, "", dayjs().startOf("M").format(DATE_FORMAT), dayjs().add(3, "M").endOf("M").format(DATE_FORMAT), "-1", "-1");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Row>
|
||||
<Col md={20} lg={20} xxl={20}>
|
||||
<SearchForm
|
||||
initialValue={{
|
||||
dates: [dayjs().startOf("M"), dayjs().add(3, "M").endOf("M")],
|
||||
}}
|
||||
fieldsConfig={{
|
||||
shows: ["referenceNo", "dates", "airticket_state", "plan_state"],
|
||||
fieldProps: {
|
||||
referenceNo: { label: "搜索计划" },
|
||||
dates: { label: "出发日期", col: 8 },
|
||||
},
|
||||
}}
|
||||
onSubmit={(err, formVal, filedsVal) => {
|
||||
getPlanList(travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.endtime, formVal.plan_state, formVal.airticket_state);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={4} lg={4} xxl={4}>
|
||||
<Space>
|
||||
<Button icon={<AuditOutlined />} onClick={() => navigate(`/airticket/invoice`)}>
|
||||
报账
|
||||
</Button>
|
||||
<Button icon={<AuditOutlined />} onClick={() => navigate(`/airticket/invoicepaid`)}>
|
||||
汇款记录
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col md={24} lg={24} xxl={24}>
|
||||
<Table bordered={true} rowKey="id" columns={planListColumns} dataSource={planList} loading={loading} pagination={{ defaultPageSize: 20, showTotal: showTotal }} />
|
||||
<TableExportBtn btnTxt="导出计划" label={`机票计划`} {...{ columns: planListColumns, dataSource: planList }} />
|
||||
</Col>
|
||||
<Col md={24} lg={24} xxl={24}></Col>
|
||||
</Row>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
export default Airticket;
|
@ -0,0 +1,137 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Grid, Divider, Layout, Spin, Input, Col, Row, Space, Checkbox, Table, Button, App } from "antd";
|
||||
import { PhoneOutlined, CustomerServiceOutlined, FrownTwoTone, LikeTwoTone } from "@ant-design/icons";
|
||||
import { useParams, useHref, useNavigate, NavLink } from "react-router-dom";
|
||||
import { isEmpty, formatColonTime, formatDate, isNotEmpty } from "@/utils/commons";
|
||||
import { DATE_FORMAT } from "@/config";
|
||||
import dayjs from "dayjs";
|
||||
import SearchForm from "@/components/SearchForm";
|
||||
import BackBtn from "@/components/BackBtn";
|
||||
import { TableExportBtn } from "@/components/Data";
|
||||
import useInvoiceStore from "@/stores/Invoice";
|
||||
import { fetchInvoicePaidDetail } from "@/stores/Invoice";
|
||||
|
||||
import airTicketStore from "@/stores/Airticket";
|
||||
import { usingStorage } from "@/hooks/usingStorage";
|
||||
|
||||
const InvoicePaid = props => {
|
||||
const navigate = useNavigate();
|
||||
const { notification } = App.useApp();
|
||||
const { travelAgencyId } = usingStorage();
|
||||
const [invoicePaidDetail, setInvoicePaidDetail] = useState([]);
|
||||
const [invoiceNO, setInvoiceNO] = useState([]); //显示账单编号
|
||||
const [loading, invoicePaid, fetchInvoicePaid] = useInvoiceStore(state => [state.loading, state.invoicePaid, state.fetchInvoicePaid]);
|
||||
const showTotal = total => `Total ${total} items`;
|
||||
const showTotal_detail = total => `Total ${total} items`;
|
||||
useEffect(() => {
|
||||
// fetchInvoicePaid(travelAgencyId, "", dayjs().subtract(2, "M").startOf("M").format(DATE_FORMAT), dayjs().endOf("M").format(DATE_FORMAT));
|
||||
}, []);
|
||||
|
||||
const invoicePaidColumns = [
|
||||
{
|
||||
title: "编号",
|
||||
dataIndex: "fl_finaceNo",
|
||||
key: "fl_finaceNo",
|
||||
},
|
||||
{
|
||||
title: "报账日期",
|
||||
key: "fl_adddate",
|
||||
dataIndex: "fl_adddate",
|
||||
render: (text, record) => (isNotEmpty(text) ? formatDate(new Date(text)) : ""),
|
||||
},
|
||||
{
|
||||
title: "团数",
|
||||
key: "fcount",
|
||||
dataIndex: "fcount",
|
||||
},
|
||||
{
|
||||
title: "总额",
|
||||
key: "pSum",
|
||||
dataIndex: "pSum",
|
||||
//render: (text, record) => (isNotEmpty(record.GMD_Currency) ? record.GMD_Currency + " " + text : text),
|
||||
},
|
||||
{
|
||||
title: "查看",
|
||||
key: "pSum",
|
||||
dataIndex: "pSum",
|
||||
render: (text, record) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
fetchInvoicePaidDetail(travelAgencyId, record.key).then(res => setInvoicePaidDetail(res));
|
||||
setInvoiceNO(record.fl_finaceNo);
|
||||
}}>
|
||||
查看明细
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const invoicePaidDetailColumns = [
|
||||
{
|
||||
title: "团号",
|
||||
dataIndex: "fl2_GroupName",
|
||||
key: "fl2_GroupName",
|
||||
},
|
||||
{
|
||||
title: "金额",
|
||||
key: "fl2_price",
|
||||
dataIndex: "fl2_price",
|
||||
},
|
||||
{
|
||||
title: "报账日期",
|
||||
key: "fl2_ArriveDate",
|
||||
dataIndex: "fl2_ArriveDate",
|
||||
render: (text, record) => (isNotEmpty(text) ? formatDate(new Date(text)) : ""),
|
||||
},
|
||||
{
|
||||
title: "顾问",
|
||||
dataIndex: "fl2_wl",
|
||||
key: "fl2_wl",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Row>
|
||||
<Col md={20} lg={20} xxl={20}>
|
||||
<SearchForm
|
||||
initialValue={{
|
||||
dates: [dayjs().subtract(2, "M").startOf("M"), dayjs().endOf("M")],
|
||||
}}
|
||||
fieldsConfig={{
|
||||
shows: ["dates"],
|
||||
fieldProps: {
|
||||
dates: { col: 10, label: "报账日期" },
|
||||
},
|
||||
}}
|
||||
onSubmit={(err, formVal) => {
|
||||
fetchInvoicePaid(travelAgencyId, "", formVal.startdate, formVal.enddate);
|
||||
setInvoicePaidDetail([]);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={4} lg={4} xxl={4}>
|
||||
<BackBtn to={"/airticket"} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col md={24} lg={16} xxl={16}>
|
||||
<Divider orientation="left">汇款列表</Divider>
|
||||
<Table bordered columns={invoicePaidColumns} dataSource={invoicePaid} loading={loading} pagination={{ defaultPageSize: 20, showTotal: showTotal }} />
|
||||
</Col>
|
||||
<Col md={24} lg={4} xxl={4}></Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col md={24} lg={20} xxl={20}>
|
||||
<Divider orientation="left">账单明细 {invoiceNO}</Divider>
|
||||
<Table bordered columns={invoicePaidDetailColumns} dataSource={invoicePaidDetail} pagination={{ defaultPageSize: 100, showTotal: showTotal_detail }} />
|
||||
<TableExportBtn btnTxt="导出账单明细" label={`机票账单`} {...{ columns: invoicePaidDetailColumns, dataSource: invoicePaidDetail }} />
|
||||
</Col>
|
||||
<Col md={24} lg={4} xxl={4}></Col>
|
||||
</Row>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
export default InvoicePaid;
|
@ -1,16 +0,0 @@
|
||||
import { useRouteError } from "react-router-dom";
|
||||
|
||||
export default function ErrorPage() {
|
||||
const error = useRouteError();
|
||||
console.error(error);
|
||||
|
||||
return (
|
||||
<div id="error-page">
|
||||
<h1>Oops!</h1>
|
||||
<p>Sorry, an unexpected error has occurred.</p>
|
||||
<p>
|
||||
<i>{error.statusText || error.message}</i>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
import { useParams, NavLink, useNavigate } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Row, Col, Space, Table, Image,App } from "antd";
|
||||
import { formatDate, isNotEmpty } from "@/utils/commons";
|
||||
import SearchForm from "@/components/SearchForm";
|
||||
import dayjs from "dayjs";
|
||||
import BackBtn from "@/components/BackBtn";
|
||||
import { fetchInvoiceDetail } from "@/stores/Invoice";
|
||||
import useInvoiceStore from "@/stores/Invoice";
|
||||
import { usingStorage } from "@/hooks/usingStorage";
|
||||
|
||||
function History() {
|
||||
const { travelAgencyId } = usingStorage();
|
||||
const { GMDSN, GSN } = useParams();
|
||||
const [dataLoading, setDataLoading] = useState(false);
|
||||
const [invoiceZDDetail, setInvoiceZDDetail] = useState([]);
|
||||
const { notification } = App.useApp();
|
||||
|
||||
useEffect(() => {
|
||||
defaultShow();
|
||||
}, [GMDSN, GSN]);
|
||||
|
||||
function defaultShow() {
|
||||
setDataLoading(true);
|
||||
|
||||
fetchInvoiceDetail(travelAgencyId, GMDSN, GSN)
|
||||
.then((json) => {
|
||||
//console.log("id:"+travelAgencyId+",gmdsn:"+GMDSN+",GSN:"+GSN+"#13"+json);
|
||||
setInvoiceZDDetail(json.invoiceZDDetail);
|
||||
})
|
||||
.catch((ex) => {
|
||||
notification.error({
|
||||
message: `Notification`,
|
||||
description: ex.message,
|
||||
placement: "top",
|
||||
duration: 4,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setDataLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
const invoicePaidColumns = [
|
||||
{
|
||||
title: "ID",
|
||||
dataIndex: "GMD_SN",
|
||||
key: "GMD_SN",
|
||||
},
|
||||
{
|
||||
title: "Due Date",
|
||||
key: "GMD_PayDate",
|
||||
dataIndex: "GMD_PayDate",
|
||||
render: (text, record) =>
|
||||
isNotEmpty(text) ? formatDate(new Date(text)) : "",
|
||||
},
|
||||
{
|
||||
title: "Amount",
|
||||
key: "GMD_Cost",
|
||||
dataIndex: "GMD_Cost",
|
||||
},
|
||||
{
|
||||
title: "Currency",
|
||||
key: "GMD_Currency",
|
||||
dataIndex: "GMD_Currency",
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
key: "FKState",
|
||||
dataIndex: "FKState",
|
||||
render: (text, record) => (isNotEmpty(record.FKState) ? invoiceStatus(record.FKState) : text),
|
||||
},
|
||||
{
|
||||
title: "Invoice",
|
||||
key: "GMD_Pic",
|
||||
dataIndex: "GMD_Pic",
|
||||
render: showPIc,
|
||||
},
|
||||
];
|
||||
|
||||
const invoiceStatus = (FKState) => {
|
||||
switch (FKState - 1) {
|
||||
case 1:
|
||||
return "Submitted";
|
||||
case 2:
|
||||
return "Travel Advisor";
|
||||
case 3:
|
||||
return "Finance Dept";
|
||||
case 4:
|
||||
return "Paid";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
function showPIc(text, record) {
|
||||
let strPic = record.GMD_Pic;
|
||||
//console.log(JSON.parse(strPic));
|
||||
if (isNotEmpty(strPic)) {
|
||||
return JSON.parse(strPic).map((item, index) => {
|
||||
return <Image key={index} width={90} src={item.url} />;
|
||||
});
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={20}></Col>
|
||||
<Col span={4}>
|
||||
<BackBtn />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col md={24} lg={24} xxl={24}>
|
||||
<Table
|
||||
rowKey={"GMD_SN"}
|
||||
bordered
|
||||
columns={invoicePaidColumns}
|
||||
dataSource={invoiceZDDetail}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default History;
|
@ -1,67 +1,63 @@
|
||||
import { NavLink, useNavigate ,useParams} from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { toJS } from "mobx";
|
||||
import { Row, Col, Space, Button, Table, Typography } from "antd";
|
||||
import { useStore } from "@/stores/StoreContext.js";
|
||||
import * as config from "@/config";
|
||||
import { formatDate, isNotEmpty } from "@/utils/commons";
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Row, Col, Space, Table, Typography } from 'antd';
|
||||
import { formatDate, isNotEmpty } from '@/utils/commons';
|
||||
import BackBtn from '@/components/BackBtn';
|
||||
import { fetchInvoicePaidDetail } from '@/stores/Invoice';
|
||||
import { usingStorage } from '@/hooks/usingStorage';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
function PaidDetail() {
|
||||
const navigate = useNavigate();
|
||||
const { authStore, invoiceStore } = useStore();
|
||||
const { invoicePaidDetail } = invoiceStore;
|
||||
const {travelAgencyId, } = usingStorage();
|
||||
const { flid } = useParams();
|
||||
|
||||
const [invoicePaidDetail, setInvoicePaidDetail] = useState([]);
|
||||
useEffect(() => {
|
||||
invoiceStore.fetchInvoicePaidDetail(authStore.login.travelAgencyId,flid);
|
||||
|
||||
fetchInvoicePaidDetail(travelAgencyId, flid).then(res => setInvoicePaidDetail(res));
|
||||
}, [flid]);
|
||||
|
||||
const invoicePaidColumns = [
|
||||
{
|
||||
title: "Ref.NO",
|
||||
dataIndex: "fl2_GroupName",
|
||||
key: "fl2_GroupName",
|
||||
title: 'Ref.NO',
|
||||
dataIndex: 'fl2_GroupName',
|
||||
key: 'fl2_GroupName',
|
||||
},
|
||||
{
|
||||
title: "Arrival date",
|
||||
key: "fl2_ArriveDate",
|
||||
dataIndex: "fl2_ArriveDate",
|
||||
render: (text, record) => (isNotEmpty(text) ? formatDate(new Date(text)) : ""),
|
||||
title: 'Arrival date',
|
||||
key: 'fl2_ArriveDate',
|
||||
dataIndex: 'fl2_ArriveDate',
|
||||
render: (text, record) => (isNotEmpty(text) ? formatDate(new Date(text)) : ''),
|
||||
},
|
||||
{
|
||||
title: "Payment amount",
|
||||
key: "fl2_price",
|
||||
dataIndex: "fl2_price",
|
||||
title: 'Payment amount',
|
||||
key: 'fl2_price',
|
||||
dataIndex: 'fl2_price',
|
||||
},
|
||||
{
|
||||
title: "Currency",
|
||||
key: "fl2_memo",
|
||||
dataIndex: "fl2_memo",
|
||||
title: 'Currency',
|
||||
key: 'fl2_memo',
|
||||
dataIndex: 'fl2_memo',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Space direction='vertical' style={{ width: '100%' }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={20}>
|
||||
</Col>
|
||||
<Col span={20}></Col>
|
||||
<Col span={4}>
|
||||
<Button type="link" onClick={() => navigate("/invoice/paid")}>
|
||||
Back
|
||||
</Button>
|
||||
<BackBtn />
|
||||
</Col>
|
||||
</Row>
|
||||
<Title level={3}></Title>
|
||||
<Row>
|
||||
<Col md={24} lg={24} xxl={24}>
|
||||
<Table bordered columns={invoicePaidColumns} dataSource={toJS(invoicePaidDetail)} />
|
||||
<Table bordered columns={invoicePaidColumns} dataSource={invoicePaidDetail} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(PaidDetail);
|
||||
export default PaidDetail;
|
||||
|
@ -1,42 +1,44 @@
|
||||
import { NavLink, useParams } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { toJS } from "mobx";
|
||||
import { Row, Col, Space, Button, Table, Input, Typography, Badge, Divider } from "antd";
|
||||
import { useStore } from "@/stores/StoreContext.js";
|
||||
import * as config from "@/config";
|
||||
import * as comm from "@/utils/commons";
|
||||
import dayjs from "dayjs";
|
||||
import { NavLink, useParams } from 'react-router-dom';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Row, Col, Space, Typography, Divider } from 'antd';
|
||||
import * as comm from '@/utils/commons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { fetchNoticeDetail } from '@/stores/Notice';
|
||||
import BackBtn from '@/components/BackBtn';
|
||||
import { usingStorage } from '@/hooks/usingStorage';
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
function Detail() {
|
||||
const { noticeStore, authStore } = useStore();
|
||||
const { noticeInfo } = noticeStore;
|
||||
const { t } = useTranslation();
|
||||
const { CCP_BLID } = useParams();
|
||||
const {userId} = usingStorage();
|
||||
|
||||
const [noticeInfo, setNoticeInfo] = useState({});
|
||||
useEffect(() => {
|
||||
console.info("notice detail .useEffect " + CCP_BLID);
|
||||
noticeStore.getNoticeDetail(authStore.login.userId, CCP_BLID);
|
||||
// console.info("notice detail .useEffect " + CCP_BLID);
|
||||
fetchNoticeDetail(userId, CCP_BLID).then((res) => {
|
||||
setNoticeInfo(res);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Space direction='vertical' style={{ width: '100%' }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={4}></Col>
|
||||
<Col span={16}>
|
||||
<Title level={1}>{noticeInfo.CCP_BLTitle}</Title>
|
||||
<Divider orientation="right">{noticeInfo.CCP_LastEditTime}</Divider>
|
||||
<Divider orientation='right'>{noticeInfo.CCP_LastEditTime}</Divider>
|
||||
<Paragraph>
|
||||
<div dangerouslySetInnerHTML={{ __html: comm.escape2Html(noticeInfo.CCP_BLContent) }}></div>
|
||||
</Paragraph>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<NavLink to="/notice">Back</NavLink>
|
||||
<BackBtn />
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Detail);
|
||||
export default Detail;
|
||||
|
@ -0,0 +1,200 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { App, Empty, Button, Collapse, Table, Space } from 'antd';
|
||||
import { useProductsTypes, useProductsAuditStatesMapVal } from '@/hooks/useProductsSets';
|
||||
import SecondHeaderWrapper from '@/components/SecondHeaderWrapper';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useProductsStore, { postProductsQuoteAuditAction, } from '@/stores/Products/Index';
|
||||
import { cloneDeep, isEmpty, isNotEmpty } from '@/utils/commons';
|
||||
import useAuthStore from '@/stores/Auth';
|
||||
import RequireAuth from '@/components/RequireAuth';
|
||||
import { PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_AUDIT, PERM_PRODUCTS_OFFER_PUT } from '@/config';
|
||||
import Header from './Detail/Header';
|
||||
import dayjs from 'dayjs';
|
||||
import { usingStorage } from '@/hooks/usingStorage';
|
||||
|
||||
const PriceTable = ({ productType, dataSource, refresh }) => {
|
||||
const { t } = useTranslation('products');
|
||||
const { travel_agency_id, use_year, audit_state } = useParams();
|
||||
const isPermitted = useAuthStore(state => state.isPermitted);
|
||||
const [loading, activeAgency] = useProductsStore((state) => [state.loading, state.activeAgency]);
|
||||
const [setEditingProduct] = useProductsStore((state) => [state.setEditingProduct]);
|
||||
const { message, notification } = App.useApp();
|
||||
const stateMapVal = useProductsAuditStatesMapVal();
|
||||
|
||||
const [renderData, setRenderData] = useState(dataSource);
|
||||
|
||||
// console.log(dataSource);
|
||||
|
||||
const handleAuditPriceItem = (state, row, rowIndex) => {
|
||||
postProductsQuoteAuditAction(state, { id: row.id, travel_agency_id: activeAgency.travel_agency_id })
|
||||
.then((json) => {
|
||||
if (json.errcode === 0) {
|
||||
message.success(json.errmsg);
|
||||
|
||||
if (typeof refresh === 'function') {
|
||||
// refresh(); // debug: 不要刷新, 等太久
|
||||
// const newData = structuredClone(renderData);
|
||||
const newData = cloneDeep(renderData);
|
||||
newData.splice(rowIndex, 1, {...row, audit_state_id: state, });
|
||||
setRenderData(newData);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((ex) => {
|
||||
notification.error({
|
||||
message: 'Notification',
|
||||
description: ex.message,
|
||||
placement: 'top',
|
||||
duration: 4,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const rowStyle = (r, tri) => {
|
||||
const trCls = tri%2 !== 0 ? ' bg-stone-50' : ''; // 奇偶行
|
||||
const [infoI, quoteI] = r.rowSpanI;
|
||||
const bigTrCls = quoteI === 0 && tri !== 0 ? 'border-collapse border-double border-0 border-t-4 border-stone-300' : ''; // 合并行双下划线
|
||||
const editedCls = (r.audit_state_id <= 0 && isNotEmpty(r.lastedit_changed)) ? '!bg-red-100' : ''; // <=待审核, 变更: 红色
|
||||
const editedCls_ = isNotEmpty(r.lastedit_changed) ? (r.audit_state_id === 0 ? '!bg-red-100' : '!bg-sky-100') : '';
|
||||
return [trCls, bigTrCls, editedCls].join(' ');
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ key: 'title', dataIndex: ['info', 'title'], width: '16rem', title: t('Title'), onCell: (r, index) => ({ rowSpan: r.rowSpan, }), className: 'bg-white', render: (text, r) => {
|
||||
const title = text || r.lgc_details?.['2']?.title || r.lgc_details?.['1']?.title || '';
|
||||
const itemLink = isPermitted(PERM_PRODUCTS_OFFER_AUDIT) ? `/products/${travel_agency_id}/${use_year}/${audit_state}/edit` : isPermitted(PERM_PRODUCTS_OFFER_PUT) ? `/products/edit` : '';
|
||||
return isNotEmpty(itemLink) ? <span onClick={() => setEditingProduct({info: r.info})}><Link to={itemLink} >{title}</Link></span> : title;
|
||||
} },
|
||||
// ...(productType === 'B' ? [{ key: 'km', dataIndex: ['info', 'km'], title: t('KM')}] : []),
|
||||
{ key: 'adult', title: t('AgeType.Adult'), render: (_, { adult_cost, currency, unit_id, unit_name }) => `${adult_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}` },
|
||||
{ key: 'child', title: t('AgeType.Child'), render: (_, { child_cost, currency, unit_id, unit_name }) => `${child_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}` },
|
||||
// {key: 'unit', title: t('Unit'), },
|
||||
{
|
||||
key: 'groupSize',
|
||||
dataIndex: ['group_size_min'],
|
||||
title: t('group_size'),
|
||||
render: (_, { group_size_min, group_size_max }) => `${group_size_min} - ${group_size_max}`,
|
||||
},
|
||||
{
|
||||
key: 'useDates',
|
||||
dataIndex: ['use_dates_start'],
|
||||
title: t('use_dates'),
|
||||
render: (_, { use_dates_start, use_dates_end, weekdays }) => `${use_dates_start} ~ ${use_dates_end}`, // + (weekdays ? `, ${t('OnWeekdays')}${weekdays}` : ''),
|
||||
},
|
||||
{ key: 'weekdays', dataIndex: ['weekdays'], title: t('Weekdays'), render: (text, r) => text || t('Unlimited') },
|
||||
{
|
||||
key: 'state',
|
||||
title: t('State'),
|
||||
render: (_, r) => {
|
||||
const stateCls = ` text-${stateMapVal[`${r.audit_state_id}`]?.color} `;
|
||||
return <span className={stateCls}>{stateMapVal[`${r.audit_state_id}`]?.label}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'action',
|
||||
render: (_, r, ri) =>
|
||||
(Number(r.audit_state_id)) === 0 ? (
|
||||
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
|
||||
<Space>
|
||||
<Button onClick={() => handleAuditPriceItem('2', r, ri)}>✔</Button>
|
||||
<Button onClick={() => handleAuditPriceItem('3', r, ri)}>✖</Button>
|
||||
</Space>
|
||||
</RequireAuth>
|
||||
) : null,
|
||||
},
|
||||
];
|
||||
return <Table size={'small'} className='border-collapse' rowHoverable={false} rowClassName={rowStyle} pagination={false} {...{ columns, }} dataSource={renderData} rowKey={(r) => r.id} />;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const TypesPanels = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [loading, agencyProducts] = useProductsStore((state) => [state.loading, state.agencyProducts]);
|
||||
// console.log(agencyProducts);
|
||||
const productsTypes = useProductsTypes();
|
||||
const [activeKey, setActiveKey] = useState([]);
|
||||
const [showTypes, setShowTypes] = useState([]);
|
||||
useEffect(() => {
|
||||
// 只显示有产品的类型; 展开产品的价格表, 合并名称列; 转化为价格主表, 携带产品属性信息
|
||||
const hasDataTypes = Object.keys(agencyProducts);
|
||||
const _show = productsTypes
|
||||
.filter((kk) => hasDataTypes.includes(kk.value))
|
||||
.map((ele) => ({
|
||||
...ele,
|
||||
extra: t('Table.Total', { total: agencyProducts[ele.value].length }),
|
||||
children: (
|
||||
<PriceTable
|
||||
// loading={loading}
|
||||
productType={ele.value}
|
||||
dataSource={agencyProducts[ele.value].reduce(
|
||||
(r, c, ri) =>
|
||||
r.concat(
|
||||
c.quotation.map((q, i) => ({
|
||||
...q,
|
||||
weekdays: q.weekdays
|
||||
.split(',')
|
||||
.filter(Boolean)
|
||||
.map((w) => t(`weekdaysShort.${w}`))
|
||||
.join(', '),
|
||||
info: c.info,
|
||||
lgc_details: c.lgc_details.reduce((rlgc, clgc) => ({...rlgc, [clgc.lgc]: clgc}), {}),
|
||||
rowSpan: i === 0 ? c.quotation.length : 0,
|
||||
rowSpanI: [ri, i],
|
||||
}))
|
||||
),
|
||||
[]
|
||||
)}
|
||||
refresh={props.refresh}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
setShowTypes(_show);
|
||||
|
||||
setActiveKey(isEmpty(_show) ? [] : [_show[0].key]);
|
||||
return () => {};
|
||||
}, [productsTypes, agencyProducts]);
|
||||
|
||||
const onCollapseChange = (_activeKey) => {
|
||||
setActiveKey(_activeKey);
|
||||
};
|
||||
return isEmpty(agencyProducts) ? <Empty /> : <Collapse items={showTypes} activeKey={activeKey} onChange={onCollapseChange} />;
|
||||
};
|
||||
|
||||
const Audit = ({ ...props }) => {
|
||||
const { notification, modal } = App.useApp()
|
||||
const isPermitted = useAuthStore(state => state.isPermitted);
|
||||
const { travel_agency_id, use_year, audit_state } = useParams();
|
||||
const [activeAgency, getAgencyProducts] = useProductsStore((state) => [state.activeAgency, state.getAgencyProducts]);
|
||||
const [loading, setLoading] = useProductsStore(state => [state.loading, state.setLoading]);
|
||||
const { travelAgencyId } = usingStorage();
|
||||
|
||||
const handleGetAgencyProducts = async ({pick_year, pick_agency, pick_state}={}) => {
|
||||
const year = pick_year || use_year || dayjs().year();
|
||||
const agency = pick_agency || travel_agency_id || travelAgencyId;
|
||||
const state = pick_state ?? audit_state;
|
||||
getAgencyProducts({ travel_agency_id: agency, use_year: year, audit_state: state }).catch(ex => {
|
||||
setLoading(false);
|
||||
notification.error({
|
||||
message: 'Notification',
|
||||
description: ex.message,
|
||||
placement: 'top',
|
||||
duration: 4,
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecondHeaderWrapper loading={loading} backTo={isPermitted(PERM_PRODUCTS_MANAGEMENT) ? `/products` : false} header={<Header title={activeAgency.travel_agency_name} refresh={handleGetAgencyProducts} />} >
|
||||
{/* <PrintContractPDF /> */}
|
||||
|
||||
<TypesPanels refresh={handleGetAgencyProducts} />
|
||||
</SecondHeaderWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Audit;
|
@ -0,0 +1,58 @@
|
||||
import { useState } from 'react';
|
||||
import { App, Divider, Empty, Flex } from 'antd';
|
||||
import { isEmpty } from '@/utils/commons';
|
||||
import SecondHeaderWrapper from '@/components/SecondHeaderWrapper';
|
||||
import Header from './Detail/Header';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useProductsStore from '@/stores/Products/Index';
|
||||
import dayjs from 'dayjs';
|
||||
import { usingStorage } from '@/hooks/usingStorage';
|
||||
import ProductsTree from './Detail/ProductsTree';
|
||||
import ProductInfo from './Detail/ProductInfo';
|
||||
import NewProductModal from './Detail/NewProductModal';
|
||||
|
||||
function Detail() {
|
||||
const { notification, modal } = App.useApp();
|
||||
const { travel_agency_id, audit_state, use_year } = useParams();
|
||||
const [addProductVisible, setAddProductVisible] = useState(false);
|
||||
const [agencyProducts, switchParams] = useProductsStore((state) => [state.agencyProducts, state.switchParams]);
|
||||
const [getAgencyProducts, activeAgency] = useProductsStore((state) => [state.getAgencyProducts, state.activeAgency]);
|
||||
const [loading, setLoading] = useProductsStore((state) => [state.loading, state.setLoading]);
|
||||
|
||||
const { travelAgencyId } = usingStorage();
|
||||
const handleGetAgencyProducts = async ({ pick_year, pick_agency, pick_state } = {}) => {
|
||||
const year = pick_year || use_year || switchParams.use_year || dayjs().year();
|
||||
const agency = pick_agency || travel_agency_id || travelAgencyId;
|
||||
const state = pick_state ?? audit_state;
|
||||
const param = { travel_agency_id: agency, use_year: year, audit_state: state };
|
||||
// setEditingProduct({});
|
||||
getAgencyProducts(param).catch((ex) => {
|
||||
setLoading(false);
|
||||
notification.error({
|
||||
message: 'Notification',
|
||||
description: ex.message,
|
||||
placement: 'top',
|
||||
duration: 4,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SecondHeaderWrapper
|
||||
loading={loading}
|
||||
backTo={false}
|
||||
header={<Header title={activeAgency.travel_agency_name} refresh={handleGetAgencyProducts} handleNewProduct={() => setAddProductVisible(true)} />}>
|
||||
<>
|
||||
<Flex gap={10} className='h-full'>
|
||||
{/* onNodeSelect={handleNodeSelect} */}
|
||||
<ProductsTree className='basis-80 sticky top-0 overflow-y-auto shrink-0' style1={{ height: 'calc(100vh - 150px)' }} />
|
||||
<Divider type={'vertical'} className='mx-1 h-auto' />
|
||||
<div className=' flex-auto overflow-auto '>
|
||||
<ProductInfo />
|
||||
</div>
|
||||
</Flex>
|
||||
</>
|
||||
</SecondHeaderWrapper>
|
||||
);
|
||||
}
|
||||
export default Detail;
|
@ -0,0 +1,100 @@
|
||||
import { useState } from 'react'
|
||||
import { Form, Modal, Input, Button, Flex, App } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useProductsStore from '@/stores/Products/Index'
|
||||
import { useProductsTypesMapVal } from '@/hooks/useProductsSets'
|
||||
import RequireAuth from '@/components/RequireAuth'
|
||||
import { PERM_PRODUCTS_OFFER_PUT } from '@/config'
|
||||
|
||||
export const ContractRemarksModal = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { notification } = App.useApp()
|
||||
const productsTypesMapVal = useProductsTypesMapVal()
|
||||
const [getRemarkList, saveOrUpdateRemark] = useProductsStore((state) => [
|
||||
state.getRemarkList, state.saveOrUpdateRemark
|
||||
])
|
||||
|
||||
const [isRemarksModalOpen, setRemarksModalOpen] = useState(false)
|
||||
const [remarksForm] = Form.useForm()
|
||||
|
||||
const onRemarksFinish = () => {
|
||||
const remarkList = remarksForm.getFieldsValue().remarkList
|
||||
saveOrUpdateRemark(remarkList)
|
||||
.then(() => {
|
||||
setRemarksModalOpen(false)
|
||||
notification.info({
|
||||
message: 'Notification',
|
||||
description: '合同备注保存成功',
|
||||
placement: 'top',
|
||||
})
|
||||
})
|
||||
.catch(ex => {
|
||||
notification.error({
|
||||
message: 'Notification',
|
||||
description: ex.message,
|
||||
placement: 'top',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleContractRemarks = () => {
|
||||
getRemarkList()
|
||||
.then(list => {
|
||||
remarksForm.setFieldsValue({remarkList:list})
|
||||
setRemarksModalOpen(true)
|
||||
})
|
||||
.catch(ex => {
|
||||
notification.error({
|
||||
message: 'Notification',
|
||||
description: ex.message,
|
||||
placement: 'top',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const getFieldLabel = (field) => {
|
||||
const remarkList = remarksForm.getFieldsValue([['remarkList']]).remarkList
|
||||
return productsTypesMapVal[remarkList[field.key].product_type_id]?.label
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT}>
|
||||
<Button size='small' onClick={handleContractRemarks}>{t('products:ContractRemarks')}</Button>
|
||||
</RequireAuth>
|
||||
|
||||
<Modal
|
||||
centered
|
||||
title={t('products:ContractRemarks')}
|
||||
width={'640px'}
|
||||
open={isRemarksModalOpen}
|
||||
onOk={() => onRemarksFinish()}
|
||||
onCancel={() => setRemarksModalOpen(false)}
|
||||
destroyOnClose
|
||||
forceRender
|
||||
>
|
||||
<Form
|
||||
labelCol={{ span: 3 }}
|
||||
wrapperCol={{ span: 20 }}
|
||||
form={remarksForm}
|
||||
name='remarksForm'
|
||||
autoComplete='off'
|
||||
>
|
||||
<Form.List name='remarkList'>
|
||||
{(fields) => (
|
||||
<Flex gap='middle' vertical>
|
||||
{fields.map((field) => (
|
||||
<Form.Item label={getFieldLabel(field)} name={[field.name, 'Memo']} key={field.key}>
|
||||
<Input.TextArea rows={2}></Input.TextArea>
|
||||
</Form.Item>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ContractRemarksModal
|
@ -0,0 +1,166 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { App, Form, Modal, DatePicker, Divider, Switch } from 'antd';
|
||||
import { isEmpty, objectMapper } from '@/utils/commons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DeptSelector from '@/components/DeptSelector';
|
||||
import ProductsTypesSelector from '@/components/ProductsTypesSelector';
|
||||
import VendorSelector from '@/components/VendorSelector';
|
||||
import dayjs from 'dayjs';
|
||||
import arraySupport from 'dayjs/plugin/arraySupport';
|
||||
import { copyAgencyDataAction } from '@/stores/Products/Index';
|
||||
|
||||
import useAuthStore from '@/stores/Auth';
|
||||
import RequireAuth from '@/components/RequireAuth';
|
||||
import { PERM_PRODUCTS_MANAGEMENT } from '@/config';
|
||||
|
||||
dayjs.extend(arraySupport);
|
||||
|
||||
export const CopyProductsForm = ({ action, initialValues, onFormInstanceReady, source, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const isPermitted = useAuthStore((state) => state.isPermitted);
|
||||
|
||||
useEffect(() => {
|
||||
onFormInstanceReady(form);
|
||||
}, []);
|
||||
|
||||
const onValuesChange = (changeValues, allValues) => {};
|
||||
return (
|
||||
<Form layout='horizontal' form={form} name='form_in_modal' initialValues={initialValues} onValuesChange={onValuesChange} >
|
||||
{action === '#' && <Form.Item name='agency' label={`${t('products:CopyFormMsg.target')}${t('products:Vendor')}`} rules={[{ required: true, message: t('products:CopyFormMsg.requiredVendor') }]}>
|
||||
<VendorSelector mode={null} placeholder={t('products:Vendor')} />
|
||||
</Form.Item>}
|
||||
<Form.Item name={`products_types`} label={t('products:ProductType')} >
|
||||
<ProductsTypesSelector maxTagCount={1} mode={'multiple'} placeholder={t('All')} />
|
||||
</Form.Item>
|
||||
{action === '#' && <RequireAuth subject={PERM_PRODUCTS_MANAGEMENT}>
|
||||
<Form.Item name={`dept`} label={t('products:Dept')} rules={[{ required: false, message: t('products:CopyFormMsg.requiredDept') }]}>
|
||||
<DeptSelector isLeaf={true} />
|
||||
</Form.Item>
|
||||
</RequireAuth>}
|
||||
<Form.Item name={'source_use_year'} label={`${t('products:CopyFormMsg.Source')}${t('products:UseYear')}`} initialValue={dayjs([source.sourceYear, 1, 1])} rules={[{ required: true,}]}>
|
||||
<DatePicker picker='year' allowClear />
|
||||
</Form.Item>
|
||||
<Form.Item name={'target_use_year'} label={`${t('products:CopyFormMsg.target')}${t('products:UseYear')}`} rules={[{ required: true,}]}>
|
||||
<DatePicker picker='year' allowClear />
|
||||
{/* disabledDate={(current) => current <= dayjs([source.sourceYear, 12, 31])} */}
|
||||
</Form.Item>
|
||||
<Form.Item name={'with_quote'} label={`${t('products:CopyFormMsg.withQuote')}`}>
|
||||
<Switch checkedChildren={'含报价金额'} unCheckedChildren={'仅人等+日期'} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
const formValuesMapper = (values) => {
|
||||
const destinationObject = {
|
||||
'agency': {
|
||||
key: 'target_agency',
|
||||
transform: (value) => {
|
||||
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '';
|
||||
},
|
||||
},
|
||||
'source_use_year': [{ key: 'source_use_year', transform: (arrVal) => (arrVal ? arrVal.format('YYYY') : '') }],
|
||||
'target_use_year': [{ key: 'target_use_year', transform: (arrVal) => (arrVal ? arrVal.format('YYYY') : '') }],
|
||||
'products_types': {
|
||||
key: 'products_types',
|
||||
transform: (value) => {
|
||||
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '-1';
|
||||
},
|
||||
},
|
||||
'dept': {
|
||||
key: 'dept',
|
||||
transform: (value) => {
|
||||
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '';
|
||||
},
|
||||
},
|
||||
'with_quote': { key: 'with_quote', transform: (value) => (value ? 1 : 0) },
|
||||
};
|
||||
let dest = {};
|
||||
const { agency, year, ...omittedValue } = values;
|
||||
dest = { ...omittedValue, ...objectMapper(values, destinationObject) };
|
||||
for (const key in dest) {
|
||||
if (Object.prototype.hasOwnProperty.call(dest, key)) {
|
||||
dest[key] = typeof dest[key] === 'string' ? (dest[key] || '').trim() : dest[key];
|
||||
}
|
||||
}
|
||||
// omit empty
|
||||
// Object.keys(dest).forEach((key) => (dest[key] == null || dest[key] === '' || dest[key].length === 0) && delete dest[key]);
|
||||
return dest;
|
||||
};
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const CopyProductsFormModal = ({ source, action = '#' | 'o', open, onSubmit, onCancel, initialValues, loading, copyModalVisible, setCopyModalVisible }) => {
|
||||
const { t } = useTranslation();
|
||||
const { notification, message } = App.useApp();
|
||||
const [formInstance, setFormInstance] = useState();
|
||||
|
||||
const [copyLoading, setCopyLoading] = useState(false);
|
||||
const handleCopyAgency = async (param) => {
|
||||
param.target_agency = isEmpty(param.target_agency) ? source.sourceAgency.travel_agency_id : param.target_agency;
|
||||
setCopyLoading(true);
|
||||
// console.log(param);
|
||||
// const toID = param.target_agency;
|
||||
const success = await copyAgencyDataAction({...param, source_agency: source.sourceAgency.travel_agency_id}).catch(ex => {
|
||||
notification.error({
|
||||
message: 'Notification',
|
||||
description: ex.message,
|
||||
placement: 'top',
|
||||
duration: 4,
|
||||
})
|
||||
});
|
||||
setCopyLoading(false);
|
||||
success ? message.success(t('Success')) : message.error(t('Failed'));
|
||||
|
||||
if (success && typeof onSubmit === 'function') {
|
||||
onSubmit(param);
|
||||
}
|
||||
// setCopyModalVisible(false);
|
||||
// navigate(`/products/${toID}/${searchValues.use_year || 'all'}/${searchValues.audit_state || 'all'}/edit`);
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
width={600}
|
||||
open={open}
|
||||
title={`${t('Copy')}${t('products:#')}${t('products:Offer')}`}
|
||||
okText='确认'
|
||||
// cancelText='Cancel'
|
||||
okButtonProps={{
|
||||
autoFocus: true,
|
||||
}}
|
||||
confirmLoading={copyLoading}
|
||||
onCancel={() => {
|
||||
onCancel();
|
||||
formInstance?.resetFields();
|
||||
}}
|
||||
destroyOnClose
|
||||
onOk={async () => {
|
||||
try {
|
||||
const values = await formInstance?.validateFields();
|
||||
// formInstance?.resetFields();
|
||||
const dest = formValuesMapper(values);
|
||||
handleCopyAgency(dest);
|
||||
} catch (error) {
|
||||
console.log('Failed:', error);
|
||||
}
|
||||
}}>
|
||||
<RequireAuth subject={PERM_PRODUCTS_MANAGEMENT}>
|
||||
<div className='py-2'>
|
||||
{t('products:CopyFormMsg.Source')}: {source.sourceAgency.travel_agency_name}
|
||||
<Divider type={'vertical'} />
|
||||
{source.sourceYear}
|
||||
</div>
|
||||
</RequireAuth>
|
||||
<CopyProductsForm action={action}
|
||||
source={source}
|
||||
initialValues={initialValues}
|
||||
onFormInstanceReady={(instance) => {
|
||||
setFormInstance(instance);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
export default CopyProductsFormModal;
|
@ -0,0 +1,176 @@
|
||||
import { useEffect, useState, useSyncExternalStore } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { App, Table, Button, Modal, Popconfirm } from 'antd';
|
||||
import { getAgencyProductExtrasAction, searchPublishedProductsAction, addProductExtraAction, delProductExtrasAction } from '@/stores/Products/Index';
|
||||
import { cloneDeep, pick } from '@/utils/commons';
|
||||
import SearchForm from '@/components/SearchForm';
|
||||
|
||||
import RequireAuth from '@/components/RequireAuth';
|
||||
import { PERM_PRODUCTS_MANAGEMENT } from '@/config';
|
||||
import { useProductsTypesMapVal } from '@/hooks/useProductsSets';
|
||||
import { usingStorage } from '@/hooks/usingStorage';
|
||||
import useProductsStore from '@/stores/Products/Index';
|
||||
|
||||
const NewAddonModal = ({ onPick, ...props }) => {
|
||||
// const { travel_agency_id, use_year } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const { notification, message } = App.useApp();
|
||||
|
||||
const [{ travel_agency_id, use_year }] = useProductsStore((state) => [state.switchParams]);
|
||||
|
||||
const productsTypesMapVal = useProductsTypesMapVal();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false); // bind loading
|
||||
const [searchLoading, setSearchLoading] = useState(false);
|
||||
const [searchResult, setSearchResult] = useState([]);
|
||||
|
||||
const onSearchProducts = async (values) => {
|
||||
const copyObject = cloneDeep(values);
|
||||
const { starttime, endtime, year, ...param } = copyObject;
|
||||
setSearchLoading(true);
|
||||
setSearchResult([]);
|
||||
const search_year = year || use_year;
|
||||
const result = await searchPublishedProductsAction({ ...param, use_year: search_year, travel_agency_id });
|
||||
setSearchResult(result);
|
||||
setSearchLoading(false);
|
||||
};
|
||||
const handleAddExtras = async (item) => {
|
||||
if (typeof onPick === 'function') {
|
||||
onPick(item);
|
||||
}
|
||||
};
|
||||
|
||||
const searchResultColumns = [
|
||||
{ key: 'ptype', dataIndex: 'type', width: '6rem', title: t('products:ProductType'), render: (text, r) => productsTypesMapVal[text]?.label || text },
|
||||
{ key: 'code', dataIndex: 'code', width: '6rem', title: t('products:Code') },
|
||||
{ key: 'title', dataIndex: 'title', width: '16rem', title: t('products:Title') },
|
||||
// {
|
||||
// title: t('products:price'),
|
||||
// dataIndex: ['quotation', '0', 'adult_cost'],
|
||||
// width: '10rem',
|
||||
// render: (_, { quotation }) => `${quotation[0].adult_cost} ${quotation[0].currency} / ${quotation[0].unit_name}`,
|
||||
// },
|
||||
{
|
||||
key: 'action',
|
||||
title: '',
|
||||
width: 150,
|
||||
render: (_, record) => (
|
||||
<Button className='text-primary' onClick={() => handleAddExtras(record)}>
|
||||
绑定此项目
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
const paginationProps = {
|
||||
showTotal: (total) => t('Table.Total', { total }),
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Button type='primary' onClick={() => setOpen(true)} className='mt-2'>
|
||||
{t('New')} {t('products:EditComponents.Extras')}
|
||||
</Button>
|
||||
|
||||
<Modal width={'95%'} style={{ top: 20 }} open={open} title={'添加附加'} footer={false} onCancel={() => setOpen(false)} destroyOnClose>
|
||||
<SearchForm
|
||||
fieldsConfig={{
|
||||
shows: ['year', 'keyword', 'products_types', 'city'], // 'dates',
|
||||
fieldProps: {
|
||||
year: { rules: [{ required: true }] },
|
||||
keyword: { label: t('products:Title'), col: 4 },
|
||||
},
|
||||
// sort: { keyword: 100 },
|
||||
}}
|
||||
initialValue={
|
||||
{
|
||||
// dates: [dayjs().subtract(2, 'M').startOf('M'), dayjs().endOf('M')],
|
||||
// year: dayjs().add(1, 'year'),
|
||||
}
|
||||
}
|
||||
onSubmit={(err, formVal, filedsVal) => {
|
||||
onSearchProducts(formVal);
|
||||
}}
|
||||
/>
|
||||
<Table
|
||||
size={'small'}
|
||||
key={'searchProductsTable'}
|
||||
rowKey={'id'}
|
||||
loading={searchLoading}
|
||||
dataSource={searchResult}
|
||||
columns={searchResultColumns}
|
||||
pagination={searchResult.length <= 10 ? false : paginationProps}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const Extras = ({ productId, onChange, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
const { notification, message } = App.useApp();
|
||||
|
||||
// const { travel_agency_id, use_year } = useParams();
|
||||
const { travelAgencyId } = usingStorage();
|
||||
const [{travel_agency_id, use_year}] = useProductsStore((state) => [state.switchParams]);
|
||||
|
||||
const [extrasData, setExtrasData] = useState([]);
|
||||
|
||||
const handleGetAgencyProductExtras = async () => {
|
||||
setExtrasData([]);
|
||||
// console.log('handleGetAgencyProductExtras', productId);
|
||||
const data = await getAgencyProductExtrasAction({ id: productId, travel_agency_id: travel_agency_id || travelAgencyId, use_year });
|
||||
setExtrasData(data);
|
||||
};
|
||||
|
||||
const handleNewAddOn = async (item) => {
|
||||
// setExtrasData(prev => [].concat(prev, [item]));
|
||||
// todo: 提交后端; 重复绑定同一个
|
||||
const _item = pick(item, ['id', 'title', 'code']);
|
||||
const newSuccess = await addProductExtraAction({ travel_agency_id, id: productId, extras: [_item] });
|
||||
newSuccess ? message.success(`${t('Success')}`) : message.error(`${t('Failed')}`);
|
||||
await handleGetAgencyProductExtras();
|
||||
}
|
||||
|
||||
const handleDelAddon = async (item) => {
|
||||
const delSuccess = await delProductExtrasAction({ travel_agency_id, id: productId, del_extras_ids: [item.id] });
|
||||
delSuccess ? message.success(`${t('Success')}`) : message.error(`${t('Failed')}`);
|
||||
await handleGetAgencyProductExtras();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (productId) handleGetAgencyProductExtras();
|
||||
|
||||
return () => {};
|
||||
}, [productId]);
|
||||
|
||||
const columns = [
|
||||
{ title: t('products:Title'), dataIndex: ['info', 'title'], width: '16rem', },
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'operation',
|
||||
width: '4rem',
|
||||
render: (_, r) => (
|
||||
<Popconfirm title={t('sureDelete')} onConfirm={(e) => handleDelAddon(r.info)} okText={t('Yes')} >
|
||||
<Button size='small' type='link' danger>
|
||||
{t('Delete')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<RequireAuth subject={PERM_PRODUCTS_MANAGEMENT}>
|
||||
<h2>{t('products:EditComponents.Extras')}</h2>
|
||||
<Table dataSource={extrasData} columns={columns} bordered pagination={false} rowKey={(r) => r.info.id} />
|
||||
<NewAddonModal onPick={handleNewAddOn} />
|
||||
</RequireAuth>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Extras;
|
@ -0,0 +1,312 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams, Link, useNavigate, useLocation } from "react-router-dom";
|
||||
import { App, Button, Divider, Popconfirm, Select } from "antd";
|
||||
import { ReloadOutlined } from "@ant-design/icons";
|
||||
import { useProductsAuditStatesMapVal } from "@/hooks/useProductsSets";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useProductsStore, {
|
||||
postAgencyProductsAuditAction,
|
||||
postAgencyAuditAction,
|
||||
getAgencyAllExtrasAction,
|
||||
} from "@/stores/Products/Index";
|
||||
import { isEmpty, objectMapper } from "@/utils/commons";
|
||||
import useAuthStore from "@/stores/Auth";
|
||||
import RequireAuth from "@/components/RequireAuth";
|
||||
// import PrintContractPDF from './PrintContractPDF';
|
||||
import { PERM_PRODUCTS_OFFER_AUDIT, PERM_PRODUCTS_OFFER_PUT } from "@/config";
|
||||
import dayjs from "dayjs";
|
||||
import VendorSelector from "@/components/VendorSelector";
|
||||
import AuditStateSelector from "@/components/AuditStateSelector";
|
||||
import { usingStorage } from "@/hooks/usingStorage";
|
||||
|
||||
import AgencyContract from "../Print/AgencyContract";
|
||||
// import AgencyContract from "../Print/AgencyContract_v0903";
|
||||
import { saveAs } from "file-saver";
|
||||
import { Packer } from "docx";
|
||||
|
||||
const Header = ({ refresh, ...props }) => {
|
||||
const location = useLocation();
|
||||
const isEditPage = location.pathname.includes("edit");
|
||||
const showEditA = !location.pathname.includes("edit");
|
||||
const showAuditA = !location.pathname.includes("audit");
|
||||
const { travel_agency_id, use_year, audit_state } = useParams();
|
||||
const { travelAgencyId } = usingStorage();
|
||||
const { t } = useTranslation();
|
||||
const isPermitted = useAuthStore((state) => state.isPermitted);
|
||||
const [activeAgency, setActiveAgency] = useProductsStore((state) => [
|
||||
state.activeAgency,
|
||||
state.setActiveAgency,
|
||||
]);
|
||||
const [switchParams, setSwitchParams] = useProductsStore((state) => [
|
||||
state.switchParams,
|
||||
state.setSwitchParams,
|
||||
]);
|
||||
// const [activeAgencyState] = useProductsStore((state) => [state.activeAgencyState]);
|
||||
const [agencyProducts] = useProductsStore((state) => [state.agencyProducts]);
|
||||
const stateMapVal = useProductsAuditStatesMapVal();
|
||||
const { message, notification } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const yearOptions = [];
|
||||
const currentYear = switchParams.use_year || dayjs().year();
|
||||
const baseYear = use_year
|
||||
? Number(use_year === "all" ? currentYear : use_year)
|
||||
: currentYear;
|
||||
for (let i = currentYear - 5; i <= baseYear + 5; i++) {
|
||||
yearOptions.push({ label: i, value: i });
|
||||
}
|
||||
|
||||
const { getRemarkList } = useProductsStore((selector) => ({
|
||||
getRemarkList: selector.getRemarkList,
|
||||
}));
|
||||
|
||||
const [param, setParam] = useState({
|
||||
pick_year: baseYear,
|
||||
pick_agency: travel_agency_id,
|
||||
});
|
||||
const [pickYear, setPickYear] = useState(baseYear);
|
||||
const [pickAgency, setPickAgency] = useState({
|
||||
value: activeAgency.travel_agency_id,
|
||||
label: activeAgency.travel_agency_name,
|
||||
});
|
||||
const [pickAuditState, setPickAuditState] = useState();
|
||||
useEffect(() => {
|
||||
const _param = objectMapper(param, {
|
||||
pick_year: "use_year",
|
||||
pick_agency: "travel_agency_id",
|
||||
pick_state: "audit_state",
|
||||
});
|
||||
setSwitchParams({
|
||||
..._param,
|
||||
travel_agency_id: _param?.travel_agency_id || travelAgencyId,
|
||||
});
|
||||
refresh(param);
|
||||
|
||||
return () => {};
|
||||
}, [param]);
|
||||
|
||||
const emptyPickState = { value: "", label: t("products:State") };
|
||||
useEffect(() => {
|
||||
const baseState = audit_state
|
||||
? audit_state === "all"
|
||||
? emptyPickState
|
||||
: stateMapVal[`${audit_state}`]
|
||||
: emptyPickState;
|
||||
if (isEmpty(pickAuditState)) {
|
||||
setPickAuditState(baseState);
|
||||
}
|
||||
return () => {};
|
||||
}, [audit_state, stateMapVal]);
|
||||
|
||||
const handleYearChange = (value) => {
|
||||
setPickYear(value);
|
||||
setParam((pre) => ({ ...pre, ...{ pick_year: value } }));
|
||||
};
|
||||
const handleAuditStateChange = (labelValue) => {
|
||||
const { value } = labelValue || emptyPickState;
|
||||
setPickAuditState(labelValue || emptyPickState);
|
||||
setParam((pre) => ({ ...pre, ...{ pick_state: value } }));
|
||||
};
|
||||
|
||||
const handleAgencyChange = ({ label, value }) => {
|
||||
setPickAgency({ label, value });
|
||||
setActiveAgency({ travel_agency_id: value, travel_agency_name: label });
|
||||
setParam((pre) => ({ ...pre, ...{ pick_agency: value } }));
|
||||
};
|
||||
|
||||
const handleAuditAgency = (state) => {
|
||||
// const s = Object.keys(agencyProducts).map((typeKey) => {
|
||||
|
||||
// });
|
||||
postAgencyProductsAuditAction(state, {
|
||||
travel_agency_id: activeAgency.travel_agency_id,
|
||||
use_year: switchParams.use_year,
|
||||
})
|
||||
.then((json) => {
|
||||
if (json.errcode === 0) {
|
||||
message.success(json.errmsg);
|
||||
if (typeof refresh === "function") {
|
||||
refresh(param);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((ex) => {
|
||||
notification.error({
|
||||
message: "Notification",
|
||||
description: ex.message,
|
||||
placement: "top",
|
||||
duration: 4,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmitForAudit = () => {
|
||||
postAgencyAuditAction(activeAgency.travel_agency_id, switchParams.use_year)
|
||||
.then((json) => {
|
||||
if (json.errcode === 0) {
|
||||
message.success(t("Success"));
|
||||
if (typeof refresh === "function") {
|
||||
refresh(param);
|
||||
const auditPagePath = isPermitted(PERM_PRODUCTS_OFFER_AUDIT)
|
||||
? `/products/${activeAgency.travel_agency_id}/${switchParams.use_year}/all/audit`
|
||||
: isPermitted(PERM_PRODUCTS_OFFER_PUT)
|
||||
? `/products/audit`
|
||||
: "";
|
||||
navigate(auditPagePath);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((ex) => {
|
||||
notification.error({
|
||||
message: "Notification",
|
||||
description: ex.message,
|
||||
placement: "top",
|
||||
duration: 4,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
// await refresh();
|
||||
const agencyExtras = await getAgencyAllExtrasAction(switchParams);
|
||||
const remarks = await getRemarkList()
|
||||
const documentCreator = new AgencyContract();
|
||||
const doc = documentCreator.create([
|
||||
switchParams,
|
||||
activeAgency,
|
||||
agencyProducts,
|
||||
agencyExtras,
|
||||
remarks
|
||||
]);
|
||||
|
||||
const _d = dayjs().format("YYYYMMDD_HH.mm.ss.SSS"); // Date.now().toString(32)
|
||||
Packer.toBlob(doc).then((blob) => {
|
||||
saveAs(
|
||||
blob,
|
||||
`${activeAgency.travel_agency_name}${pickYear}年地接合同-${_d}.docx`
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-end items-center gap-4 h-full">
|
||||
<div className="grow">
|
||||
<h2 className="m-0 leading-tight">
|
||||
{isPermitted(PERM_PRODUCTS_OFFER_AUDIT) ? (
|
||||
<VendorSelector
|
||||
value={{
|
||||
label: activeAgency.travel_agency_name,
|
||||
value: activeAgency.travel_agency_id,
|
||||
}}
|
||||
onChange={handleAgencyChange}
|
||||
allowClear={false}
|
||||
mode={null}
|
||||
className="w-72"
|
||||
size="large"
|
||||
variant={"borderless"}
|
||||
/>
|
||||
) : (
|
||||
activeAgency.travel_agency_name
|
||||
)}
|
||||
<Divider type={"vertical"} />
|
||||
<Select
|
||||
options={yearOptions}
|
||||
variant={"borderless"}
|
||||
className="w-24"
|
||||
size="large"
|
||||
value={pickYear}
|
||||
onChange={handleYearChange}
|
||||
/>
|
||||
<Divider type={"vertical"} />
|
||||
<AuditStateSelector
|
||||
variant={"borderless"}
|
||||
className="w-32"
|
||||
size="large"
|
||||
value={pickAuditState}
|
||||
onChange={handleAuditStateChange}
|
||||
/>
|
||||
{/* <Divider type={'vertical'} />
|
||||
{(use_year || '').replace('all', '')} */}
|
||||
<Button
|
||||
onClick={() => refresh(param)}
|
||||
type="text"
|
||||
className="text-primary round-none"
|
||||
icon={<ReloadOutlined />}
|
||||
/>
|
||||
</h2>
|
||||
</div>
|
||||
{/* todo: export, 审核完成之后才能导出 */}
|
||||
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
|
||||
<Button size="small" onClick={handleDownload}>
|
||||
{t("Export")} .docx
|
||||
</Button>
|
||||
{/* <PrintContractPDF /> */}
|
||||
</RequireAuth>
|
||||
{/* {activeAgencyState === 0 && ( */}
|
||||
<>
|
||||
<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT}>
|
||||
<Popconfirm
|
||||
title={t("products:sureSubmitAudit")}
|
||||
onConfirm={handleSubmitForAudit}
|
||||
okText={t("Yes")}
|
||||
placement={"bottomLeft"}
|
||||
>
|
||||
<Button size="small" type={"primary"}>
|
||||
{t("Submit")}
|
||||
{t("Audit")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</RequireAuth>
|
||||
</>
|
||||
{/* )} */}
|
||||
{showEditA && (
|
||||
<Link
|
||||
className="px-2"
|
||||
to={
|
||||
isPermitted(PERM_PRODUCTS_OFFER_AUDIT)
|
||||
? `/products/${activeAgency.travel_agency_id}/${pickYear}/${audit_state}/edit`
|
||||
: `/products/edit`
|
||||
}
|
||||
>
|
||||
{t("Edit")}
|
||||
</Link>
|
||||
)}
|
||||
{showAuditA && (
|
||||
<Link
|
||||
className="px-2"
|
||||
to={
|
||||
isPermitted(PERM_PRODUCTS_OFFER_AUDIT)
|
||||
? `/products/${activeAgency.travel_agency_id}/${pickYear}/${audit_state}/audit`
|
||||
: `/products/audit`
|
||||
}
|
||||
>
|
||||
{t("products:AuditRes")}
|
||||
</Link>
|
||||
)}
|
||||
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
|
||||
<Button
|
||||
size="small"
|
||||
type={"primary"}
|
||||
onClick={() => handleAuditAgency("1")}
|
||||
>
|
||||
{t("products:auditStateAction.Published")}
|
||||
</Button>
|
||||
</RequireAuth>
|
||||
{/* <Button size='small' type={'primary'} ghost onClick={() => handleAuditAgency('2')}>
|
||||
{t('products:auditStateAction.Approved')}
|
||||
</Button> */}
|
||||
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
|
||||
<Button
|
||||
size="small"
|
||||
type={"primary"}
|
||||
danger
|
||||
ghost
|
||||
onClick={() => handleAuditAgency("3")}
|
||||
>
|
||||
{t("products:auditStateAction.Rejected")}
|
||||
</Button>
|
||||
</RequireAuth>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Header;
|
@ -0,0 +1,135 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Form, Modal, Input, Button } from 'antd';
|
||||
import { objectMapper } from '@/utils/commons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ProductsTypesSelector from '@/components/ProductsTypesSelector';
|
||||
import useProductsStore from '@/stores/Products/Index';
|
||||
import { useNewProductRecord, useProductsTypesMapVal } from '@/hooks/useProductsSets';
|
||||
import { useDefaultLgc } from '@/i18n/LanguageSwitcher';
|
||||
import RequireAuth from '@/components/RequireAuth';
|
||||
import { PERM_PRODUCTS_OFFER_PUT, PERM_PRODUCTS_NEW } from '@/config';
|
||||
|
||||
export const NewProductsForm = ({ initialValues, onFormInstanceReady, ...props }) => {
|
||||
const { t } = useTranslation('products');
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
onFormInstanceReady(form);
|
||||
}, []);
|
||||
|
||||
const [pickType, setPickType] = useState({ value: '6' });
|
||||
const onValuesChange = (changeValues, allValues) => {
|
||||
if ('products_type' in changeValues) {
|
||||
setPickType(changeValues.products_type);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form layout='horizontal' form={form} name='new_product_in_modal' initialValues={initialValues} onValuesChange={onValuesChange}>
|
||||
<Form.Item name={`products_type`} label={t('products:ProductType')} rules={[{ required: true }]} tooltip={false}>
|
||||
<ProductsTypesSelector maxTagCount={1} mode={null} placeholder={t('common:All')} />
|
||||
</Form.Item>
|
||||
<Form.Item name={`title`} label={t('products:Title')} rules={[{ required: true }]} tooltip={t(`FormTooltip.NewTitle.${pickType.value}`)} dependencies={['products_type']}>
|
||||
{/* ${pickType.value} */}
|
||||
<Input placeholder={t(`FormTooltip.NewTitle.${pickType.value}`)} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
const formValuesMapper = (values) => {
|
||||
const destinationObject = {
|
||||
'products_types': {
|
||||
key: 'products_types',
|
||||
transform: (value) => {
|
||||
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '-1';
|
||||
},
|
||||
},
|
||||
};
|
||||
let dest = {};
|
||||
const { ...omittedValue } = values;
|
||||
dest = { ...omittedValue, ...objectMapper(values, destinationObject) };
|
||||
for (const key in dest) {
|
||||
if (Object.prototype.hasOwnProperty.call(dest, key)) {
|
||||
dest[key] = typeof dest[key] === 'string' ? (dest[key] || '').trim() : dest[key];
|
||||
}
|
||||
}
|
||||
// omit empty
|
||||
// Object.keys(dest).forEach((key) => (dest[key] == null || dest[key] === '' || dest[key].length === 0) && delete dest[key]);
|
||||
return dest;
|
||||
};
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const NewProductModal = ({ initialValues }) => {
|
||||
const { t } = useTranslation();
|
||||
const [formInstance, setFormInstance] = useState();
|
||||
const [setEditingProduct] = useProductsStore((state) => [state.setEditingProduct]);
|
||||
const [switchParams] = useProductsStore((state) => [state.switchParams]);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [copyLoading, setCopyLoading] = useState(false);
|
||||
const productsTypesMapVal = useProductsTypesMapVal();
|
||||
const newProduct = useNewProductRecord();
|
||||
const { language } = useDefaultLgc();
|
||||
const handelAddProduct = (param) => {
|
||||
const copyNewProduct = structuredClone(newProduct);
|
||||
copyNewProduct.info.title = param.title;
|
||||
copyNewProduct.info.product_title = param.title;
|
||||
copyNewProduct.info.product_type_id = productsTypesMapVal[param.products_type.value].value;
|
||||
copyNewProduct.info.product_type_name = productsTypesMapVal[param.products_type.value].label;
|
||||
copyNewProduct.lgc_details[0].lgc = language;
|
||||
copyNewProduct.lgc_details[0].title = param.title;
|
||||
copyNewProduct.quotation[0].use_dates_start = `${switchParams.use_year}-01-01`;
|
||||
copyNewProduct.quotation[0].use_dates_end = `${switchParams.use_year}-12-31`;
|
||||
setEditingProduct(copyNewProduct);
|
||||
// if (typeof onSubmit === 'function') {
|
||||
// onSubmit();
|
||||
// }
|
||||
setOpen(false);
|
||||
return false;
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<RequireAuth subject={PERM_PRODUCTS_NEW}>
|
||||
<Button size='small' type={'primary'} onClick={() => setOpen(true)}>
|
||||
{t('New')}
|
||||
{t('products:#')}
|
||||
</Button>
|
||||
</RequireAuth>
|
||||
|
||||
<Modal
|
||||
width={600}
|
||||
open={open}
|
||||
title={`${t('common:New')}${t('products:#')}`}
|
||||
okButtonProps={{
|
||||
autoFocus: true,
|
||||
}}
|
||||
confirmLoading={copyLoading}
|
||||
onCancel={() => {
|
||||
// onCancel();
|
||||
setOpen(false);
|
||||
formInstance?.resetFields();
|
||||
}}
|
||||
destroyOnClose
|
||||
onOk={async () => {
|
||||
try {
|
||||
const values = await formInstance?.validateFields();
|
||||
// formInstance?.resetFields();
|
||||
const dest = formValuesMapper(values);
|
||||
handelAddProduct(dest);
|
||||
} catch (error) {
|
||||
console.log('Failed:', error);
|
||||
}
|
||||
}}>
|
||||
<NewProductsForm
|
||||
initialValues={initialValues}
|
||||
onFormInstanceReady={(instance) => {
|
||||
setFormInstance(instance);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default NewProductModal;
|
@ -0,0 +1,109 @@
|
||||
import { useState } from 'react'
|
||||
import { Input, Space } from 'antd'
|
||||
|
||||
const PriceCompactInput = (props) => {
|
||||
const { id, value = {}, onChange } = props
|
||||
const [numberStart, setNumberStart] = useState(0)
|
||||
const [numberEnd, setNumberEnd] = useState(0)
|
||||
const [audultPrice, setAudultPrice] = useState(0)
|
||||
const [childrenPrice, setChildrenPrice] = useState(0)
|
||||
const triggerChange = (changedValue) => {
|
||||
onChange?.({
|
||||
numberStart,
|
||||
numberEnd,
|
||||
audultPrice,
|
||||
childrenPrice,
|
||||
...value,
|
||||
...changedValue,
|
||||
})
|
||||
}
|
||||
const onNumberStartChange = (e) => {
|
||||
const newNumber = parseInt(e.target.value || '0', 10)
|
||||
if (Number.isNaN(newNumber)) {
|
||||
return
|
||||
}
|
||||
if (!('numberStart' in value)) {
|
||||
setNumberStart(newNumber)
|
||||
}
|
||||
triggerChange({
|
||||
numberStart: newNumber,
|
||||
})
|
||||
}
|
||||
const onNumberEndChange = (e) => {
|
||||
const newNumber = parseInt(e.target.value || '0', 10)
|
||||
if (Number.isNaN(newNumber)) {
|
||||
return
|
||||
}
|
||||
if (!('numberEnd' in value)) {
|
||||
setNumberEnd(newNumber)
|
||||
}
|
||||
triggerChange({
|
||||
numberEnd: newNumber,
|
||||
})
|
||||
}
|
||||
const onAudultPriceChange = (e) => {
|
||||
const newNumber = parseInt(e.target.value || '0', 10)
|
||||
if (Number.isNaN(newNumber)) {
|
||||
return
|
||||
}
|
||||
if (!('audultPrice' in value)) {
|
||||
setAudultPrice(newNumber)
|
||||
}
|
||||
triggerChange({
|
||||
audultPrice: newNumber,
|
||||
})
|
||||
}
|
||||
const onChildrenPriceChange = (e) => {
|
||||
const newNumber = parseInt(e.target.value || '0', 10)
|
||||
if (Number.isNaN(newNumber)) {
|
||||
return
|
||||
}
|
||||
if (!('childrenPrice' in value)) {
|
||||
setChildrenPrice(newNumber)
|
||||
}
|
||||
triggerChange({
|
||||
childrenPrice: newNumber,
|
||||
})
|
||||
}
|
||||
return (
|
||||
<Space.Compact id={id}>
|
||||
<Input
|
||||
type='text'
|
||||
value={value.numberStart || numberStart}
|
||||
onChange={onNumberStartChange}
|
||||
style={{
|
||||
width: '20%',
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
type='text'
|
||||
value={value.numberEnd || numberEnd}
|
||||
onChange={onNumberEndChange}
|
||||
style={{
|
||||
width: '40%',
|
||||
}}
|
||||
addonBefore='~'
|
||||
/>
|
||||
<Input
|
||||
type='text'
|
||||
value={value.audultPrice || audultPrice}
|
||||
onChange={onAudultPriceChange}
|
||||
style={{
|
||||
width: '70%',
|
||||
}}
|
||||
addonBefore='成人价'
|
||||
/>
|
||||
<Input
|
||||
type='text'
|
||||
value={value.childrenPrice || childrenPrice}
|
||||
onChange={onChildrenPriceChange}
|
||||
style={{
|
||||
width: '70%',
|
||||
}}
|
||||
addonBefore='儿童价'
|
||||
/>
|
||||
</Space.Compact>
|
||||
)
|
||||
}
|
||||
|
||||
export default PriceCompactInput
|
@ -0,0 +1,150 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { App, Breadcrumb, Divider } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useProductsTypesMapVal, useNewProductRecord } from '@/hooks/useProductsSets';
|
||||
import useProductsStore, { postProductsSaveAction } from '@/stores/Products/Index';
|
||||
import useAuthStore from '@/stores/Auth';
|
||||
import { PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_PUT, PERM_PRODUCTS_INFO_PUT, PERM_PRODUCTS_NEW } from '@/config';
|
||||
import { isEmpty, pick } from '@/utils/commons';
|
||||
import ProductInfoForm from './ProductInfoForm';
|
||||
import { usingStorage } from '@/hooks/usingStorage';
|
||||
import Extras from './Extras';
|
||||
import NewProductModal from './NewProductModal';
|
||||
|
||||
const ProductInfo = ({ ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
const { notification, message } = App.useApp();
|
||||
const { userId } = usingStorage();
|
||||
const isPermitted = useAuthStore((state) => state.isPermitted);
|
||||
const productsTypesMapVal = useProductsTypesMapVal();
|
||||
const newProductRecord = useNewProductRecord();
|
||||
|
||||
const [loading, setLoading, appendNewProduct] = useProductsStore((state) => [state.loading, state.setLoading, state.appendNewProduct]);
|
||||
const [activeAgency, editingProduct, setEditingProduct] = useProductsStore((state) => [state.activeAgency, state.editingProduct, state.setEditingProduct]);
|
||||
|
||||
const [extrasVisible, setExtrasVisible] = useState(false);
|
||||
const [editablePerm, setEditablePerm] = useState(false);
|
||||
const [infoEditable, setInfoEditable] = useState(false);
|
||||
const [priceEditable, setPriceEditable] = useState(false);
|
||||
useEffect(() => {
|
||||
const topPerm = isPermitted(PERM_PRODUCTS_MANAGEMENT); // 高级权限
|
||||
|
||||
const hasHT = (editingProduct?.info?.htid || 0) > 0;
|
||||
// const hasAuditPer = isPermitted(PERM_PRODUCTS_OFFER_AUDIT);
|
||||
const hasEditPer = isPermitted(PERM_PRODUCTS_INFO_PUT) || isPermitted(PERM_PRODUCTS_NEW); // || isPermitted(PERM_PRODUCTS_OFFER_PUT);
|
||||
|
||||
setEditablePerm(topPerm || hasEditPer);
|
||||
// setEditable(topPerm || (hasAuditPer ? true : (!hasHT && hasEditPer)));
|
||||
// setEditable(true); // debug: 0
|
||||
// console.log('editable', hasAuditPer, (notAudit && hasEditPer));
|
||||
setInfoEditable(topPerm || (!hasHT && hasEditPer));
|
||||
|
||||
const _priceEditable = [-1, 3].includes(activeAgency?.audit_state_id) || isEmpty(editingProduct?.info?.id);
|
||||
const hasPricePer = isPermitted(PERM_PRODUCTS_OFFER_PUT);
|
||||
// setPriceEditable(topPerm || (_priceEditable && hasPricePer));
|
||||
setPriceEditable(topPerm || (hasPricePer));
|
||||
// setPriceEditable(true); // debug: 0
|
||||
|
||||
const showExtras = topPerm && hasHT; // !isEmpty(editingProduct) &&
|
||||
setExtrasVisible(showExtras);
|
||||
|
||||
setLgcEdits({});
|
||||
setInfoEditStatus('');
|
||||
return () => {};
|
||||
}, [activeAgency, editingProduct]);
|
||||
|
||||
const [infoEditStatus, setInfoEditStatus] = useState('');
|
||||
const [lgcEdits, setLgcEdits] = useState({});
|
||||
const onValuesChange = (changedValues, forms) => {
|
||||
// console.log('onValuesChange', changedValues);
|
||||
if ('product_title' in changedValues) {
|
||||
setInfoEditStatus('2');
|
||||
setLgcEdits({...lgcEdits, '2': {'edit_status': '2'}});
|
||||
}
|
||||
if ('lgc_details_mapped' in changedValues) {
|
||||
const lgc = Object.keys(changedValues.lgc_details_mapped)[0];
|
||||
setLgcEdits({...lgcEdits, [lgc]: {'edit_status': '2'}});
|
||||
} else {
|
||||
setInfoEditStatus('2');
|
||||
}
|
||||
};
|
||||
|
||||
const onSave = async (err, values, forms) => {
|
||||
values.travel_agency_id = activeAgency.travel_agency_id;
|
||||
const copyNewProduct = structuredClone(newProductRecord);
|
||||
const poster = {
|
||||
// ...(topPerm ? { } : { 'audit_state': -1 }), // 高级权限: 不变更状态值
|
||||
// "create_date": dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
// "created_by": userId,
|
||||
'travel_agency_id': activeAgency.travel_agency_id,
|
||||
// "travel_agency_name": "",
|
||||
// "lastedit_changed": "",
|
||||
"edit_status": infoEditStatus || editingProduct.info.edit_status,
|
||||
};
|
||||
const copyFields = pick(editingProduct.info, ['product_type_id']); // 'title',
|
||||
const readyToSubInfo = { ...copyNewProduct.info, ...editingProduct.info, title: editingProduct.info.product_title, ...values.info, ...copyFields, ...poster };
|
||||
// console.log('onSave', editingProduct.info, readyToSubInfo);
|
||||
|
||||
/** lgc_details */
|
||||
const prevLgcDetailsMapped = editingProduct.lgc_details.reduce((r, c) => ({ ...r, [c.lgc]: c }), {});
|
||||
const mergedLgc = { ...prevLgcDetailsMapped, ...values.lgc_details_mapped, };
|
||||
for (const lgcKey in lgcEdits) {
|
||||
if (Object.prototype.hasOwnProperty.call(lgcEdits, lgcKey)) {
|
||||
const element = lgcEdits[lgcKey];
|
||||
mergedLgc[lgcKey].edit_status = element?.edit_status || values.lgc_details_mapped[lgcKey]?.edit_status || '2';
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('before save', '\n lgcEdits:', lgcEdits, '\n mergedLgc', mergedLgc);
|
||||
// return false; // debug: 0
|
||||
/** 提交保存 */
|
||||
setLoading(true);
|
||||
const { success, result } = await postProductsSaveAction({
|
||||
travel_agency_id: activeAgency.travel_agency_id,
|
||||
info: readyToSubInfo,
|
||||
lgc_details: Object.values(mergedLgc),
|
||||
quotation: values.quotation.map((q) => ({ ...q, unit: Number(q.unit || q.unit_id), unit_id: Number(q.unit_id) })), // || editingProduct.quotation, // 没改动, 就用原来的
|
||||
}).catch((ex) => {
|
||||
setLoading(false);
|
||||
notification.error({
|
||||
message: 'Notification',
|
||||
description: ex.message,
|
||||
placement: 'top',
|
||||
duration: 4,
|
||||
});
|
||||
});
|
||||
setLoading(false);
|
||||
success ? message.success(t('Success')) : message.error(t('Failed'));
|
||||
// 保存后更新数据
|
||||
// result.quotation = isEmpty(result.quotation) ? editingProduct.quotation : result.quotation;
|
||||
result.info.htid = editingProduct?.info?.htid;
|
||||
appendNewProduct(result);
|
||||
setEditingProduct(result);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{ title: productsTypesMapVal[editingProduct?.info?.product_type_id]?.label || editingProduct?.info?.product_type_name },
|
||||
{ title: editingProduct?.info?.title ?? t('New') },
|
||||
// { title: 'htID: ' + editingProduct?.info?.htid },
|
||||
// { title: 'ID: ' + editingProduct?.info?.id },
|
||||
]}
|
||||
/>
|
||||
<Divider className='my-1' />
|
||||
{isEmpty(editingProduct) ? (
|
||||
<div className=' my-2'>
|
||||
<NewProductModal />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<h2>{t('products:EditComponents.info')}</h2>
|
||||
<ProductInfoForm {...{ editablePerm, infoEditable, priceEditable, onValuesChange }} initialValues={editingProduct?.info} onSubmit={onSave} />
|
||||
<Divider className='my-1' />
|
||||
{extrasVisible && <Extras productId={editingProduct?.info?.id} />}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ProductInfo;
|
@ -0,0 +1,434 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { App, Form, Input, Row, Col, Select, Button, InputNumber, Checkbox } from 'antd';
|
||||
import { objectMapper, isEmpty, isNotEmpty } from '@/utils/commons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useWeekdays } from '@/hooks/useDatePresets';
|
||||
import DeptSelector from '@/components/DeptSelector';
|
||||
import CitySelector from '@/components/CitySelector';
|
||||
import { useProductsTypesFieldsets } from '@/hooks/useProductsSets';
|
||||
import useProductsStore from '@/stores/Products/Index';
|
||||
import ProductInfoLgc from './ProductInfoLgc';
|
||||
import ProductInfoQuotation from './ProductInfoQuotation';
|
||||
import { useHTLanguageSetsMapVal } from '@/hooks/useHTLanguageSets';
|
||||
|
||||
const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditable, priceEditable, showSubmit, confirmText, formName, ...props }) => {
|
||||
const { notification } = App.useApp();
|
||||
const { t } = useTranslation('products');
|
||||
const HTLanguageSetsMapVal = useHTLanguageSetsMapVal();
|
||||
const [loading, editingProduct] = useProductsStore((state) => [state.loading, state.editingProduct]);
|
||||
const weekdays = useWeekdays();
|
||||
const [form] = Form.useForm();
|
||||
const { sort, hides, fieldProps, fieldComProps } = {
|
||||
sort: '',
|
||||
fieldProps: '',
|
||||
fieldComProps: '',
|
||||
hides: [],
|
||||
shows: [],
|
||||
...props.fieldsConfig,
|
||||
};
|
||||
const filedsets = useProductsTypesFieldsets(editingProduct?.info?.product_type_id);
|
||||
const shows = filedsets[0];
|
||||
|
||||
const [pickEditedInfo, setPickEditedInfo] = useState({}); // 传递联动的字段
|
||||
|
||||
// const [editable, setEditable] = useState(true);
|
||||
const [formEditable, setFormEditable] = useState(true);
|
||||
const [showSave, setShowSave] = useState(true);
|
||||
useEffect(() => {
|
||||
form.resetFields();
|
||||
form.setFieldValue('city', editingProduct?.info?.city_id ? { value: editingProduct?.info?.city_id, label: editingProduct?.info?.city_name } : undefined);
|
||||
form.setFieldValue('dept', { value: editingProduct?.info?.dept_id, label: editingProduct?.info?.dept_name });
|
||||
const lgc_details_mapped = (editingProduct?.lgc_details || []).reduce((r, c) => ({ ...r, [c.lgc]: c }), {});
|
||||
form.setFieldValue('lgc_details_mapped', lgc_details_mapped);
|
||||
form.setFieldValue('quotation', editingProduct?.quotation);
|
||||
form.setFieldValue('display_to_c', editingProduct.info?.display_to_c || '0');
|
||||
setPickEditedInfo({ ...pickEditedInfo, product_title: editingProduct?.info?.product_title });
|
||||
|
||||
setFormEditable(infoEditable || priceEditable);
|
||||
|
||||
// const editable0 = isEmpty(editingProduct) ? false : editablePerm; // 空对象未操作
|
||||
setShowSave(infoEditable || priceEditable);
|
||||
// setEditable(editable0);
|
||||
return () => {};
|
||||
}, [editingProduct, editablePerm, infoEditable, priceEditable]);
|
||||
|
||||
const onFinish = (values) => {
|
||||
console.log('Received values of form, origin form value: \n', values);
|
||||
const dest = formValuesMapper(values);
|
||||
console.log('form value send to onSubmit:\n', dest);
|
||||
if (typeof onSubmit === 'function') {
|
||||
onSubmit(null, dest, values);
|
||||
}
|
||||
};
|
||||
|
||||
const onFinishFailed = ({ values, errorFields }) => {
|
||||
console.log('form validate failed', '\nform values:', values, '\nerrorFields', errorFields);
|
||||
notification.warning({
|
||||
message: '数据未填写完整',
|
||||
// description: '数据未填写完整',
|
||||
placement: 'top',
|
||||
duration: 4,
|
||||
})
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
form.setFieldsValue({
|
||||
// 'DateType': undefined,
|
||||
});
|
||||
if (typeof onReset === 'function') {
|
||||
onReset();
|
||||
}
|
||||
};
|
||||
const onIValuesChange = (changedValues, allValues) => {
|
||||
const dest = formValuesMapper(allValues);
|
||||
// console.log('form onValuesChange', Object.keys(changedValues), changedValues);
|
||||
if ('product_title' in changedValues) {
|
||||
const editTitle = (changedValues.product_title);
|
||||
setPickEditedInfo({ ...pickEditedInfo, product_title: editTitle });
|
||||
}
|
||||
if (typeof onValuesChange === 'function') {
|
||||
onValuesChange(changedValues, dest);
|
||||
}
|
||||
};
|
||||
const onFieldsChange = (hangedFields, allFields) => {
|
||||
console.log('onFieldsChange', hangedFields, allFields);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
form={form}
|
||||
disabled={!formEditable}
|
||||
name={formName || 'product_info'}
|
||||
// preserve={false}
|
||||
onFinish={onFinish}
|
||||
onValuesChange={onIValuesChange}
|
||||
// onFieldsChange={onFieldsChange}
|
||||
initialValues={editingProduct?.info}
|
||||
onFinishFailed={onFinishFailed} scrollToFirstError >
|
||||
<Row>
|
||||
{getFields({ sort, initialValue: editingProduct?.info, hides, shows, fieldProps, fieldComProps, form, t, dataSets: { weekdays }, editable: infoEditable })}
|
||||
{/* {showSubmit && (
|
||||
<Col flex='1 0 90px' className='flex justify-end items-start'>
|
||||
<Space align='center'>
|
||||
<Button size={'middle'} type='primary' htmlType='submit' loading={loading}>
|
||||
{confirmText || t('common:Save')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
)} */}
|
||||
</Row>
|
||||
{/* <Divider className='my-1' /> */}
|
||||
<Form.Item className='mb-0'
|
||||
name={'lgc_details_mapped'}
|
||||
rules={[
|
||||
() => ({
|
||||
transform(value) {
|
||||
return Object.values(value).filter((_v) => !isEmpty(_v));
|
||||
},
|
||||
validator: async (_, valueArr) => {
|
||||
const invalidLgcName = valueArr
|
||||
.filter((l) => isEmpty(l.title))
|
||||
.map((x) => HTLanguageSetsMapVal[x.lgc].label)
|
||||
.join(', ');
|
||||
if (isNotEmpty(invalidLgcName)) {
|
||||
// Please complete multi -language information
|
||||
return Promise.reject(new Error(`请完善多语种信息: ${invalidLgcName}`));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
]}>
|
||||
<ProductInfoLgc editable={infoEditable} formInstance={form} pickEditedInfo={pickEditedInfo} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name='quotation'>
|
||||
<ProductInfoQuotation editable={priceEditable} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item hidden name={'id'} label={'ID'}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
{/* <Form.Item hidden name={'title'} label={'title'}>
|
||||
<Input />
|
||||
</Form.Item> */}
|
||||
{showSave && (
|
||||
<Form.Item>
|
||||
<div className='flex justify-around'>
|
||||
<Button type='primary' htmlType='submit' loading={loading}>
|
||||
{t('common:Save')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function getFields(props) {
|
||||
const { fieldProps, fieldComProps, form, t, dataSets } = props;
|
||||
// console.log('getFields', props.initialValue);
|
||||
const styleProps = {};
|
||||
const editableProps = (name) => {
|
||||
// const disabled = props.ignoreEditable ? false : (isEmpty(props.initialValue?.[name]) && props.editable ? false : true)
|
||||
const disabled = !props.editable;
|
||||
return { disabled, className: disabled ? '!text-slate-500' : '' };
|
||||
};
|
||||
const bigCol = 4 * 2;
|
||||
const midCol = 8;
|
||||
const layoutProps = {
|
||||
// gutter: { xs: 8, sm: 8, lg: 16 },
|
||||
lg: { span: 6 },
|
||||
md: { span: 8 },
|
||||
sm: { span: 12 },
|
||||
xs: { span: 24 },
|
||||
};
|
||||
const item = (name, sort = 0, render, col) => {
|
||||
const customCol = col || midCol;
|
||||
const mdCol = customCol * 2;
|
||||
return {
|
||||
'key': '',
|
||||
sort,
|
||||
name,
|
||||
render,
|
||||
'hide': false,
|
||||
'col': { lg: { span: customCol }, md: { span: mdCol < 8 ? 10 : mdCol > 24 ? 24 : mdCol }, flex: mdCol < 8 ? '1 0' : '' },
|
||||
};
|
||||
};
|
||||
let baseChildren = [];
|
||||
baseChildren = [
|
||||
item(
|
||||
'product_title',
|
||||
99,
|
||||
<Form.Item name='product_title' label={t('Title')} {...fieldProps.product_title} rules={[{ required: true }]} tooltip={false}>
|
||||
<Input allowClear {...fieldComProps.product_title} {...styleProps} {...editableProps('product_title')} />
|
||||
</Form.Item>,
|
||||
fieldProps?.product_title?.col || midCol
|
||||
),
|
||||
item(
|
||||
'code',
|
||||
99,
|
||||
<Form.Item name='code' label={t('Code')} {...fieldProps.code} rules={[{ required: true }]} tooltip={false}>
|
||||
<Input allowClear {...fieldComProps.code} {...styleProps} {...editableProps('code')} />
|
||||
</Form.Item>,
|
||||
fieldProps?.code?.col || midCol
|
||||
),
|
||||
item(
|
||||
'city',
|
||||
99,
|
||||
<Form.Item name='city' label={t('City')} {...fieldProps.city} rules={[{ required: true }]} tooltip={t('FormTooltip.City')}>
|
||||
<CitySelector {...styleProps} {...editableProps('city_id')} placeholder={t('FormTooltip.City')} />
|
||||
</Form.Item>,
|
||||
fieldProps?.city?.col || midCol
|
||||
),
|
||||
item(
|
||||
'dept',
|
||||
99,
|
||||
<Form.Item name='dept' label={t('Dept')} {...fieldProps.dept}>
|
||||
<DeptSelector labelInValue={false} isLeaf {...styleProps} {...editableProps('dept')} />
|
||||
</Form.Item>,
|
||||
fieldProps?.dept?.col || midCol
|
||||
),
|
||||
item(
|
||||
'duration',
|
||||
99,
|
||||
<Form.Item name='duration' label={t('Duration')} {...fieldProps.duration} rules={[{ required: true, type: 'number', min: 0}]} tooltip={false}>
|
||||
<InputNumber suffix={'H'} max={24} {...styleProps} {...editableProps('duration')} />
|
||||
{/* <Input allowClear {...fieldComProps.duration} suffix={'H'} /> */}
|
||||
</Form.Item>,
|
||||
fieldProps?.duration?.col || midCol
|
||||
),
|
||||
item(
|
||||
'km',
|
||||
99,
|
||||
<Form.Item name='km' label={t('KM')} {...fieldProps.km} rules={[{ required: true, },]} tooltip={t('FormTooltip.KM')}>
|
||||
<InputNumber suffix={'KM'} min={0.1} {...styleProps} {...editableProps('km')} placeholder={t('FormTooltip.KM')} />
|
||||
</Form.Item>,
|
||||
fieldProps?.km?.col || midCol
|
||||
),
|
||||
item(
|
||||
'recommends_rate',
|
||||
99,
|
||||
<Form.Item name='recommends_rate' label={t('RecommendsRate')} {...fieldProps.recommends_rate} tooltip={t('FormTooltip.RecommendsRate')}>
|
||||
{/* <Input placeholder={t('RecommendsRate')} allowClear /> */}
|
||||
<InputNumber {...styleProps} {...editableProps('recommends_rate')} min={1} max={1000} />
|
||||
{/* <Select
|
||||
{...styleProps}
|
||||
{...editableProps('recommends_rate')}
|
||||
style={{ width: '100%' }}
|
||||
labelInValue={false}
|
||||
options={[
|
||||
{ value: 1, label: 'Top 1' },
|
||||
{ value: 2, label: 'Top 2' },
|
||||
{ value: 3, label: 'Top 3' },
|
||||
{ value: 4, label: '4' },
|
||||
{ value: 5, label: '5' },
|
||||
]}
|
||||
/> */}
|
||||
</Form.Item>,
|
||||
fieldProps?.recommends_rate?.col || midCol
|
||||
),
|
||||
item(
|
||||
'display_to_c',
|
||||
99,
|
||||
<Form.Item
|
||||
name='display_to_c'
|
||||
label={t('DisplayToC')}
|
||||
{...fieldProps.display_to_c}
|
||||
tooltip={t('FormTooltip.DisplayToC')}
|
||||
// rules={[
|
||||
// () => ({
|
||||
// validator(_, value) {
|
||||
// if ((value || []).includes(153002) && !(value || []).includes(153001)) {
|
||||
// return Promise.reject(new Error('不允许仅报价信显示'));
|
||||
// }
|
||||
// return Promise.resolve();
|
||||
// },
|
||||
// }),
|
||||
// ]}
|
||||
>
|
||||
{/* <Checkbox.Group
|
||||
options={[
|
||||
{ value: 153001, label: '报价信不显示' },
|
||||
{ value: 153002, label: '计划不显示' },
|
||||
]}
|
||||
/> */}
|
||||
<Select
|
||||
labelInValue={false}
|
||||
options={[
|
||||
{ value: '153001', label: '在计划显示,不在报价信显示' },
|
||||
{ value: '0', label: '计划和报价信都要显示' },
|
||||
{ value: '153001, 153002', label: '计划和报价信都不用显示' },
|
||||
]}
|
||||
{...styleProps}
|
||||
{...editableProps('display_to_c')}
|
||||
/>
|
||||
</Form.Item>,
|
||||
fieldProps?.display_to_c?.col || midCol
|
||||
),
|
||||
item(
|
||||
'open_weekdays',
|
||||
99,
|
||||
<Form.Item name='open_weekdays' label={t('OpenWeekdays')} {...fieldProps.open_weekdays} tooltip={false}>
|
||||
<Checkbox.Group options={dataSets.weekdays} {...styleProps} {...editableProps('open_weekdays')} />
|
||||
</Form.Item>,
|
||||
fieldProps?.open_weekdays?.col || 24
|
||||
),
|
||||
item(
|
||||
'remarks',
|
||||
99,
|
||||
<Form.Item name='remarks' label={t('Remarks')} {...fieldProps.remarks} tooltip={t('FormTooltip.Remarks')}>
|
||||
<Input.TextArea allowClear rows={2} maxLength={2000} showCount {...fieldComProps.remarks} {...styleProps} {...editableProps('remarks')} />
|
||||
</Form.Item>,
|
||||
fieldProps?.remarks?.col || 24
|
||||
),
|
||||
];
|
||||
baseChildren = baseChildren
|
||||
.map((x) => {
|
||||
x.hide = false;
|
||||
if (props.sort === undefined) {
|
||||
return x;
|
||||
}
|
||||
const tmpSort = props.sort;
|
||||
for (const key in tmpSort) {
|
||||
if (Object.prototype.hasOwnProperty.call(tmpSort, key)) {
|
||||
if (x.name === key) {
|
||||
x.sort = tmpSort[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return x;
|
||||
})
|
||||
.map((x) => {
|
||||
if (props.hides.length === 0 && props.shows.length === 0) {
|
||||
return x;
|
||||
}
|
||||
if (props.hides.length === 0) {
|
||||
x.hide = !props.shows.includes(x.name);
|
||||
} else if (props.shows.length === 0) {
|
||||
x.hide = props.hides.includes(x.name);
|
||||
}
|
||||
return x;
|
||||
})
|
||||
.filter((x) => !x.hide)
|
||||
.sort((a, b) => {
|
||||
return a.sort < b.sort ? -1 : 1;
|
||||
});
|
||||
const children = [];
|
||||
const leftStyle = {}; // { borderRight: '1px solid #dedede' };
|
||||
for (let i = 0; i < baseChildren.length; i++) {
|
||||
let style = {}; // { padding: '0px 2px' };
|
||||
style = i % 2 === 0 && baseChildren[i].col === 12 ? { ...style, ...leftStyle } : style;
|
||||
style = !baseChildren[i].hide ? { ...style, display: 'block' } : { ...style, display: 'none' };
|
||||
const Item = (
|
||||
<Col key={String(i)} style={style} {...baseChildren[i].col} className='px-1 shrink-0 grow'>
|
||||
{baseChildren[i].render}
|
||||
</Col>
|
||||
);
|
||||
children.push(Item);
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
const formValuesMapper = (values) => {
|
||||
const destinationObject = {
|
||||
'city': [
|
||||
{ key: 'city_id', transform: (value) => value?.value || value?.key || '' },
|
||||
{ key: 'city_name', transform: (value) => value?.label || '' },
|
||||
],
|
||||
'dept': { key: 'dept_id', transform: (value) => (typeof value === 'string' ? value : value?.value || value?.key || '') },
|
||||
'open_weekdays': { key: 'open_weekdays', transform: (value) => (Array.isArray(value) ? value.join(',') : value) },
|
||||
// 'recommends_rate': { key: 'recommends_rate', transform: (value) => ((typeof value === 'string' || typeof value === 'number') ? value : value?.value || value?.key || '') },
|
||||
// 'lgc_details': [
|
||||
// {
|
||||
// key: 'lgc_details',
|
||||
// transform: (value) => {
|
||||
// const _val = value.filter((s) => s !== undefined).map((e) => ({ title: '', ...e, description: e.description || '' }));
|
||||
// return _val || '';
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// key: 'lgc_details_mapped_tmp',
|
||||
// transform: (value) => {
|
||||
// const _val = value.filter((s) => s !== undefined);
|
||||
// return _val.reduce((r, c) => ({ ...r, [c.lgc]: c }), {});
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
'lgc_details_mapped': [
|
||||
{
|
||||
key: 'lgc_details',
|
||||
transform: (value) => {
|
||||
const valueArr = Object.values(value)
|
||||
.filter((_v) => !isEmpty(_v.lgc))
|
||||
.map((e) => ({ title: '', ...e, descriptions: e.descriptions || '' }));
|
||||
return valueArr;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'lgc_details_mapped',
|
||||
transform: (value) => {
|
||||
const valueArr = Object.values(value)
|
||||
.filter((_v) => !isEmpty(_v.lgc))
|
||||
.map((e) => ({ title: '', ...e, descriptions: e.descriptions || '' }));
|
||||
return valueArr.reduce((r, c) => ({ ...r, [c.lgc]: c }), {});
|
||||
},
|
||||
},
|
||||
],
|
||||
'product_title': { key: 'title' },
|
||||
};
|
||||
let dest = {};
|
||||
const { city, dept, product_title, ...omittedValue } = values;
|
||||
dest = { ...omittedValue, ...objectMapper(values, destinationObject) };
|
||||
for (const key in dest) {
|
||||
if (Object.prototype.hasOwnProperty.call(dest, key)) {
|
||||
dest[key] = typeof dest[key] === 'string' ? (dest[key] || '').trim() : dest[key];
|
||||
}
|
||||
}
|
||||
// omit empty
|
||||
// Object.keys(dest).forEach((key) => (dest[key] == null || dest[key] === '' || dest[key].length === 0) && delete dest[key]);
|
||||
const { lgc_details, lgc_details_mapped, quotation, ...info } = dest;
|
||||
return { info, lgc_details, lgc_details_mapped, quotation };
|
||||
};
|
||||
|
||||
export default InfoForm;
|
@ -0,0 +1,185 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Input, Tabs, Modal, Select, Form } from 'antd';
|
||||
import useProductsStore from '@/stores/Products/Index';
|
||||
import { useHTLanguageSets, useHTLanguageSetsMapVal } from '@/hooks/useHTLanguageSets';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDefaultLgc } from '@/i18n/LanguageSwitcher';
|
||||
import { cloneDeep, isEmpty, isNotEmpty } from '@/utils/commons';
|
||||
|
||||
const ProductInfoLgc = ({ editable, formInstance, pickEditedInfo, ...props }) => {
|
||||
const { t } = useTranslation('products');
|
||||
const { language: languageHT } = useDefaultLgc();
|
||||
const HTLanguageSetsMapVal = useHTLanguageSetsMapVal();
|
||||
const allLgcOptions = useHTLanguageSets();
|
||||
const [editingProduct] = useProductsStore((state) => [state.editingProduct]);
|
||||
const [activeKey, setActiveKey] = useState();
|
||||
const [items, setItems] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
formInstance.setFieldValue(['lgc_details_mapped', '2', 'title'], pickEditedInfo.product_title);
|
||||
|
||||
return () => {};
|
||||
}, [pickEditedInfo.product_title]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const existsLgc = (editingProduct?.lgc_details || []).map((ele, li) => ({
|
||||
...ele,
|
||||
label: HTLanguageSetsMapVal[ele.lgc]?.label || ele.lgc,
|
||||
// key: `${editingProduct.info.id}-${ele.id}`,
|
||||
key: ele.lgc,
|
||||
closable: false, // isPermitted(PERM_PRODUCTS_MANAGEMENT) ? true : false,
|
||||
forceRender: true,
|
||||
children: (
|
||||
<Form.Item noStyle key={`${editingProduct.info.id}-${ele.id}`}>
|
||||
<Form.Item name={['lgc_details_mapped', `${ele.lgc}`, 'title']} label={t('products:Title')} initialValue={ele.title} rules={[{ required: true }]} tooltip={t(`FormTooltip.NewTitle.${editingProduct?.info?.product_type_id}`)}>
|
||||
<Input
|
||||
className={' !text-slate-600'}
|
||||
allowClear
|
||||
placeholder={t(`FormTooltip.NewTitle.${editingProduct?.info?.product_type_id}`)}
|
||||
// onChange={(e) => handleChange('title', e.target.value)}
|
||||
// disabled={ignoreEditable ? false : (!isEmpty(ele.title) || !editable)}
|
||||
// disabled={ignoreEditable ? false : !editable}
|
||||
disabled={!editable}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name={['lgc_details_mapped', `${ele.lgc}`, 'descriptions']} label={t('products:Description')} initialValue={ele.descriptions} tooltip={t('FormTooltip.Description')}>
|
||||
<Input.TextArea
|
||||
className={'!text-slate-600'}
|
||||
rows={3} maxLength={2000} showCount
|
||||
allowClear
|
||||
// onChange={(e) => handleChange('description', e.target.value)}
|
||||
// disabled={ignoreEditable ? false : (!isEmpty(ele.descriptions) || !editable)}
|
||||
// disabled={ignoreEditable ? false : !editable}
|
||||
disabled={!editable}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item hidden name={['lgc_details_mapped', `${ele.lgc}`, 'lgc']} initialValue={ele.lgc}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item hidden name={['lgc_details_mapped', `${ele.lgc}`, 'id']} initialValue={ele.id}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
),
|
||||
}));
|
||||
setItems(existsLgc);
|
||||
const pageDefaultLgcI = (editingProduct?.lgc_details || []).findIndex((ele) => ele.lgc === languageHT);
|
||||
setActiveKey(existsLgc?.[pageDefaultLgcI || 0]?.key);
|
||||
// formInstance.validateFields();
|
||||
const filterLgcOptions = allLgcOptions.filter((ele) => !existsLgc.some((item) => `${item.lgc}` === ele.value));
|
||||
setLgcOptions(filterLgcOptions);
|
||||
|
||||
return () => {};
|
||||
}, [editingProduct]);
|
||||
|
||||
const onLgcTabChange = (newActiveKey) => {
|
||||
setActiveKey(newActiveKey);
|
||||
};
|
||||
|
||||
const addLgc = (lgcItem) => {
|
||||
// const currentLgcOptions = structuredClone(lgcOptions);
|
||||
const currentLgcOptions = cloneDeep(lgcOptions);
|
||||
currentLgcOptions.splice(
|
||||
currentLgcOptions.findIndex((ele) => ele.value === lgcItem.value),
|
||||
1
|
||||
);
|
||||
setLgcOptions(currentLgcOptions);
|
||||
|
||||
const newActiveKey = lgcItem.value;
|
||||
const newPanes = [...items];
|
||||
newPanes.push({
|
||||
...lgcItem,
|
||||
forceRender: true,
|
||||
key: lgcItem.value,
|
||||
children: (
|
||||
<Form.Item noStyle>
|
||||
<Form.Item name={['lgc_details_mapped', `${lgcItem.value}`, 'title']} preserve={false} label={t('products:Title')} rules={[{ required: true }]} tooltip={t(`FormTooltip.NewTitle.${editingProduct?.info?.product_type_id}`)}>
|
||||
<Input allowClear placeholder={t(`FormTooltip.NewTitle.${editingProduct?.info?.product_type_id}`)} />
|
||||
</Form.Item>
|
||||
<Form.Item name={['lgc_details_mapped', `${lgcItem.value}`, 'descriptions']} preserve={false} label={t('products:Description')} tooltip={t('FormTooltip.Description')}>
|
||||
<Input.TextArea rows={3} maxLength={2000} showCount allowClear />
|
||||
</Form.Item>
|
||||
<Form.Item hidden name={['lgc_details_mapped', `${lgcItem.value}`, 'lgc']} preserve={false} initialValue={lgcItem.value}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item hidden name={['lgc_details_mapped', `${lgcItem.value}`, 'id']} preserve={false} initialValue={''}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
),
|
||||
});
|
||||
setItems(newPanes);
|
||||
setActiveKey(newActiveKey);
|
||||
setSelectNewLgc(null);
|
||||
};
|
||||
const remove = (targetKey) => {
|
||||
let newActiveKey = activeKey;
|
||||
let lastIndex = -1;
|
||||
items.forEach((item, i) => {
|
||||
if (item.key === targetKey) {
|
||||
lastIndex = i - 1;
|
||||
}
|
||||
});
|
||||
const newPanes = items.filter((item) => item.key !== targetKey);
|
||||
if (newPanes.length && newActiveKey === targetKey) {
|
||||
if (lastIndex >= 0) {
|
||||
newActiveKey = newPanes[lastIndex].key;
|
||||
} else {
|
||||
newActiveKey = newPanes[0].key;
|
||||
}
|
||||
}
|
||||
setItems(newPanes);
|
||||
setActiveKey(newActiveKey);
|
||||
setLgcOptions([...lgcOptions, ...items.filter((item) => item.key === targetKey)]);
|
||||
};
|
||||
const onEdit = (targetKey, action) => {
|
||||
if (action === 'add') {
|
||||
setNewLgcModalVisible(true);
|
||||
} else {
|
||||
remove(targetKey);
|
||||
formInstance.validateFields(['lgc_details_mapped']);
|
||||
}
|
||||
};
|
||||
|
||||
const [newLgcModalVisible, setNewLgcModalVisible] = useState(false);
|
||||
const [selectNewLgc, setSelectNewLgc] = useState();
|
||||
const [lgcOptions, setLgcOptions] = useState(allLgcOptions);
|
||||
const handleOk = () => {
|
||||
addLgc(selectNewLgc);
|
||||
setNewLgcModalVisible(false);
|
||||
};
|
||||
const handleCancel = () => {
|
||||
setNewLgcModalVisible(false);
|
||||
};
|
||||
const onSelectNewLgc = (lgcItem) => {
|
||||
setSelectNewLgc(lgcItem);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
type='editable-card'
|
||||
size='small'
|
||||
onChange={onLgcTabChange}
|
||||
activeKey={activeKey}
|
||||
onEdit={onEdit}
|
||||
items={items}
|
||||
hideAdd={isEmpty(lgcOptions) || !editable}
|
||||
tabPosition='top'
|
||||
/>
|
||||
<Modal title={t('LgcModal.title')} open={newLgcModalVisible} onOk={handleOk} onCancel={() => setNewLgcModalVisible(false)} destroyOnClose>
|
||||
<Select
|
||||
showSearch
|
||||
labelInValue
|
||||
style={{ width: '80%' }}
|
||||
placeholder={t('LgcModal.placeholder')}
|
||||
optionFilterProp='children'
|
||||
options={lgcOptions}
|
||||
onChange={onSelectNewLgc}
|
||||
value={selectNewLgc}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ProductInfoLgc;
|
@ -0,0 +1,495 @@
|
||||
import { useState } from 'react'
|
||||
import { Table, Form, Modal, Button, Radio, Input, Flex, Card, InputNumber, Checkbox, DatePicker, Space, App, Tooltip } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CloseOutlined, StarTwoTone, PlusOutlined, ExclamationCircleFilled, QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import { useDatePresets } from '@/hooks/useDatePresets'
|
||||
import dayjs from 'dayjs'
|
||||
import useProductsStore from '@/stores/Products/Index'
|
||||
import PriceCompactInput from '@/views/products/Detail/PriceCompactInput'
|
||||
|
||||
const { RangePicker } = DatePicker
|
||||
|
||||
const batchSetupInitialValues = {
|
||||
'defList': [
|
||||
// 旺季
|
||||
{
|
||||
'useDateList': [
|
||||
{
|
||||
'useDate': [
|
||||
dayjs().add(1, 'year').startOf('y'), dayjs().add(1, 'year').endOf('y')
|
||||
]
|
||||
}
|
||||
],
|
||||
'unitId': '0',
|
||||
'currency': 'RMB',
|
||||
'weekend': [
|
||||
],
|
||||
'priceList': [
|
||||
{
|
||||
'priceInput': {
|
||||
'numberStart': 1,
|
||||
'numberEnd': 2,
|
||||
'audultPrice': 0,
|
||||
'childrenPrice': 0
|
||||
}
|
||||
},
|
||||
{
|
||||
'priceInput': {
|
||||
'numberStart': 3,
|
||||
'numberEnd': 4,
|
||||
'audultPrice': 0,
|
||||
'childrenPrice': 0
|
||||
}
|
||||
},
|
||||
{
|
||||
'priceInput': {
|
||||
'numberStart': 5,
|
||||
'numberEnd': 6,
|
||||
'audultPrice': 0,
|
||||
'childrenPrice': 0
|
||||
}
|
||||
},
|
||||
{
|
||||
'priceInput': {
|
||||
'numberStart': 7,
|
||||
'numberEnd': 9,
|
||||
'audultPrice': 0,
|
||||
'childrenPrice': 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// 淡季
|
||||
{
|
||||
'useDateList': [
|
||||
{
|
||||
'useDate': [
|
||||
dayjs().add(1, 'year').subtract(2, 'M').startOf('M'), dayjs().add(1, 'year').endOf('M')
|
||||
]
|
||||
}
|
||||
],
|
||||
'unitId': '0',
|
||||
'currency': 'RMB',
|
||||
'weekend': [
|
||||
],
|
||||
'priceList': [
|
||||
{
|
||||
'priceInput': {
|
||||
'numberStart': 1,
|
||||
'numberEnd': 2,
|
||||
'audultPrice': 0,
|
||||
'childrenPrice': 0
|
||||
}
|
||||
},
|
||||
{
|
||||
'priceInput': {
|
||||
'numberStart': 3,
|
||||
'numberEnd': 4,
|
||||
'audultPrice': 0,
|
||||
'childrenPrice': 0
|
||||
}
|
||||
},
|
||||
{
|
||||
'priceInput': {
|
||||
'numberStart': 5,
|
||||
'numberEnd': 6,
|
||||
'audultPrice': 0,
|
||||
'childrenPrice': 0
|
||||
}
|
||||
},
|
||||
{
|
||||
'priceInput': {
|
||||
'numberStart': 7,
|
||||
'numberEnd': 9,
|
||||
'audultPrice': 0,
|
||||
'childrenPrice': 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const defaultPriceValue = {
|
||||
'priceInput': {
|
||||
'numberStart': 1,
|
||||
'numberEnd': 2,
|
||||
'audultPrice': 0,
|
||||
'childrenPrice': 0
|
||||
}
|
||||
}
|
||||
|
||||
const defaultUseDate = {
|
||||
'useDate': [dayjs().add(1, 'year').startOf('y'), dayjs().add(1, 'year').endOf('y')]
|
||||
}
|
||||
|
||||
const defaultDefinitionValue = {
|
||||
'useDateList': [defaultUseDate],
|
||||
'unitId': '0',
|
||||
'currency': 'RMB',
|
||||
'weekend': [],
|
||||
'priceList': [defaultPriceValue]
|
||||
}
|
||||
|
||||
const ProductInfoQuotation = ({ editable, ...props }) => {
|
||||
|
||||
const { onChange } = props
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isQuotationModalOpen, setQuotationModalOpen] = useState(false)
|
||||
const [isBatchSetupModalOpen, setBatchSetupModalOpen] = useState(false)
|
||||
const { modal, notification } = App.useApp()
|
||||
const [quotationForm] = Form.useForm()
|
||||
const [batchSetupForm] = Form.useForm()
|
||||
|
||||
const datePresets = useDatePresets()
|
||||
|
||||
const [quotationList, newEmptyQuotation, appendQuotationList, saveOrUpdateQuotation, deleteQuotation] =
|
||||
useProductsStore((state) => [state.quotationList, state.newEmptyQuotation, state.appendQuotationList, state.saveOrUpdateQuotation, state.deleteQuotation])
|
||||
|
||||
const triggerChange = (changedValue) => {
|
||||
onChange?.(
|
||||
changedValue
|
||||
)
|
||||
}
|
||||
|
||||
const onQuotationSeleted = async (quotation) => {
|
||||
// 把 start, end 转换为 RangePicker 数组格式
|
||||
quotation.use_dates = [dayjs(quotation.use_dates_start), dayjs(quotation.use_dates_end)]
|
||||
quotation.weekdayList = quotation.weekdays.split(',')
|
||||
quotationForm.setFieldsValue(quotation)
|
||||
setQuotationModalOpen(true)
|
||||
}
|
||||
|
||||
const onNewQuotation = () => {
|
||||
const emptyQuotation = newEmptyQuotation()
|
||||
quotationForm.setFieldsValue(emptyQuotation)
|
||||
setQuotationModalOpen(true)
|
||||
}
|
||||
|
||||
const onQuotationFinish = (values) => {
|
||||
const newList = saveOrUpdateQuotation(values)
|
||||
triggerChange(newList)
|
||||
setQuotationModalOpen(false)
|
||||
}
|
||||
|
||||
const onBatchSetupFinish = () => {
|
||||
const defList = batchSetupForm.getFieldsValue().defList
|
||||
const newList = appendQuotationList(defList)
|
||||
triggerChange(newList)
|
||||
setBatchSetupModalOpen(false)
|
||||
}
|
||||
|
||||
const onDeleteQuotation = (quotation) => {
|
||||
modal.confirm({
|
||||
title: '请确认',
|
||||
icon: <ExclamationCircleFilled />,
|
||||
content: '你要删除这条价格吗?',
|
||||
onOk() {
|
||||
deleteQuotation(quotation)
|
||||
.catch(ex => {
|
||||
notification.error({
|
||||
message: 'Notification',
|
||||
description: ex.message,
|
||||
placement: 'top',
|
||||
duration: 4,
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const quotationColumns = [
|
||||
// { title: 'id', dataIndex: 'id', width: 40, className: 'italic text-gray-400' }, // test: 0
|
||||
// { title: 'WPI_SN', dataIndex: 'WPI_SN', width: 40, className: 'italic text-gray-400' }, // test: 0
|
||||
{ title: t('products:adultPrice'), dataIndex: 'adult_cost', width: '5rem' },
|
||||
{ title: t('products:childrenPrice'), dataIndex: 'child_cost', width: '5rem' },
|
||||
{ title: t('products:currency'), dataIndex: 'currency', width: '4rem' },
|
||||
{
|
||||
title: (<>{t('products:unit_name')} <Tooltip placement='top' overlayInnerStyle={{width: '24rem'}} title={t('products:FormTooltip.PriceUnit')}><QuestionCircleOutlined className='text-gray-500' /></Tooltip> </>),
|
||||
dataIndex: 'unit_id',
|
||||
width: '6rem',
|
||||
render: (text) => t(`products:PriceUnit.${text}`), // (text === '0' ? '每人' : text === '1' ? '每团' : text),
|
||||
},
|
||||
{
|
||||
title: t('products:group_size'),
|
||||
dataIndex: 'group_size',
|
||||
width: '6rem',
|
||||
render: (_, record) => `${record.group_size_min}-${record.group_size_max}`,
|
||||
},
|
||||
|
||||
{
|
||||
title: (<>{t('products:use_dates')} <Tooltip placement='top' overlayInnerStyle={{width: '24rem'}} title={t('products:FormTooltip.UseDates')}><QuestionCircleOutlined className='text-gray-500' /></Tooltip> </>),
|
||||
dataIndex: 'use_dates',
|
||||
// width: '6rem',
|
||||
render: (_, record) => `${record.use_dates_start}-${record.use_dates_end}`,
|
||||
},
|
||||
|
||||
{ title: t('products:Weekdays'), dataIndex: 'weekdays', width: '4rem' },
|
||||
{
|
||||
title: t('products:operation'),
|
||||
dataIndex: 'operation',
|
||||
width: '10rem',
|
||||
render: (_, quotation) => {
|
||||
// const _rowEditable = [-1,3].includes(quotation.audit_state_id);
|
||||
const _rowEditable = true; // test: 0
|
||||
return (
|
||||
<Space>
|
||||
<Button type='link' disabled={!_rowEditable} onClick={() => onQuotationSeleted(quotation)}>{t('Edit')}</Button>
|
||||
<Button type='link' danger disabled={!_rowEditable} onClick={() => onDeleteQuotation(quotation)}>{t('Delete')}</Button>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>{t('products:EditComponents.Quotation')}</h2>
|
||||
<Table size='small'
|
||||
bordered
|
||||
dataSource={quotationList}
|
||||
columns={quotationColumns}
|
||||
pagination={false}
|
||||
/>
|
||||
{
|
||||
editable &&
|
||||
<Space>
|
||||
<Button onClick={() => onNewQuotation()} type='primary' ghost style={{ marginTop: 16 }}>
|
||||
{t('products:addQuotation')}
|
||||
</Button>
|
||||
<Button onClick={() => setBatchSetupModalOpen(true)} type='primary' ghost style={{ marginTop: 16, marginLeft: 16 }}>
|
||||
批量设置
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
<Modal
|
||||
centered
|
||||
title='批量设置价格'
|
||||
width={'640px'}
|
||||
open={isBatchSetupModalOpen}
|
||||
onOk={() => onBatchSetupFinish()}
|
||||
onCancel={() => setBatchSetupModalOpen(false)}
|
||||
destroyOnClose
|
||||
forceRender
|
||||
>
|
||||
<Form
|
||||
labelCol={{ span: 3 }}
|
||||
wrapperCol={{ span: 20 }}
|
||||
form={batchSetupForm}
|
||||
name='batchSetupForm'
|
||||
autoComplete='off'
|
||||
initialValues={batchSetupInitialValues}
|
||||
>
|
||||
<Form.List name='defList'>
|
||||
{(fields, { add, remove }) => (
|
||||
<Flex gap='middle' vertical>
|
||||
{fields.map((field, index) => (
|
||||
<Card
|
||||
size='small'
|
||||
title={index == 0 ? '旺季' : index == 1 ? '淡季' : '其他'}
|
||||
key={field.key}
|
||||
extra={index == 0 ? <StarTwoTone twoToneColor='#eb2f96' /> : <CloseOutlined onClick={() => {
|
||||
remove(field.name)
|
||||
}} />}
|
||||
>
|
||||
<Form.Item label='币种' name={[field.name, 'currency']}>
|
||||
<Radio.Group>
|
||||
<Radio value='RMB'>RMB</Radio>
|
||||
<Radio value='USD'>USD</Radio>
|
||||
<Radio value='THB'>THB</Radio>
|
||||
<Radio value='JPY'>JPY</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label='类型' name={[field.name, 'unitId']}>
|
||||
<Radio.Group>
|
||||
<Radio value='0'>每人</Radio>
|
||||
<Radio value='1'>每团</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label='周末' name={[field.name, 'weekend']}>
|
||||
<Checkbox.Group
|
||||
options={['5', '6', '7']}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label='有效期'>
|
||||
<Form.List name={[field.name, 'useDateList']}>
|
||||
{(useDateFieldList, useDateOptList) => (
|
||||
<Flex gap='middle' vertical>
|
||||
{useDateFieldList.map((useDateField, index) => (
|
||||
<Space key={useDateField.key}>
|
||||
<Form.Item noStyle name={[useDateField.name, 'useDate']}>
|
||||
<RangePicker style={{ width: '100%' }} allowClear={true} inputReadOnly={true} presets={datePresets} placeholder={['From', 'Thru']} />
|
||||
</Form.Item>
|
||||
{index == 0 ? <StarTwoTone twoToneColor='#eb2f96' /> : <CloseOutlined onClick={() => useDateOptList.remove(useDateField.name)} />}
|
||||
</Space>
|
||||
))}
|
||||
<Button type='dashed' icon={<PlusOutlined />} onClick={() => useDateOptList.add(defaultUseDate)} block>
|
||||
新增有效期
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form.Item>
|
||||
<Form.Item label='人等'>
|
||||
<Form.List name={[field.name, 'priceList']}>
|
||||
{(priceFieldList, priceOptList) => (
|
||||
<Flex gap='middle' vertical>
|
||||
{priceFieldList.map((priceField, index) => (
|
||||
<Space key={priceField.key}>
|
||||
<Form.Item noStyle name={[priceField.name, 'priceInput']}>
|
||||
<PriceCompactInput />
|
||||
</Form.Item>
|
||||
{index == 0 ? <StarTwoTone twoToneColor='#eb2f96' /> : <CloseOutlined onClick={() => priceOptList.remove(priceField.name)} />}
|
||||
</Space>
|
||||
))}
|
||||
<Button type='dashed' icon={<PlusOutlined />} onClick={() => priceOptList.add(defaultPriceValue)} block>
|
||||
新增人等
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
))}
|
||||
<Button type='dashed' icon={<PlusOutlined />} onClick={() => add(defaultDefinitionValue)} block>
|
||||
新增设置
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
centered
|
||||
okButtonProps={{
|
||||
autoFocus: true,
|
||||
htmlType: 'submit',
|
||||
}}
|
||||
title={t('account:detail')}
|
||||
open={isQuotationModalOpen} onCancel={() => setQuotationModalOpen(false)}
|
||||
destroyOnClose
|
||||
forceRender
|
||||
modalRender={(dom) => (
|
||||
<Form
|
||||
name='quotationForm'
|
||||
form={quotationForm}
|
||||
labelCol={{ span: 4 }}
|
||||
wrapperCol={{ span: 20 }}
|
||||
className='max-w-2xl'
|
||||
onFinish={onQuotationFinish}
|
||||
autoComplete='off'
|
||||
>
|
||||
{dom}
|
||||
</Form>
|
||||
)}
|
||||
>
|
||||
<Form.Item name='id' className='hidden' ><Input /></Form.Item>
|
||||
<Form.Item name='key' className='hidden' ><Input /></Form.Item>
|
||||
<Form.Item name='fresh' className='hidden' ><Input /></Form.Item>
|
||||
<Form.Item
|
||||
label={t('products:adultPrice')}
|
||||
name='adult_cost'
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('products:Validation.adultPrice'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('products:childrenPrice')}
|
||||
name='child_cost'
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('products:Validation.childrenPrice'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('products:currency')}
|
||||
name='currency'
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('products:Validation.currency'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value='RMB'>RMB</Radio>
|
||||
<Radio value='USD'>USD</Radio>
|
||||
<Radio value='THB'>THB</Radio>
|
||||
<Radio value='JPY'>JPY</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('products:unit_name')}
|
||||
name='unit_id'
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('products:Validation.unit_name'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value='0'>每人</Radio>
|
||||
<Radio value='1'>每团</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('products:group_size')}
|
||||
name='group_size_min'
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('products:Validation.group_size_min'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('products:group_size')}
|
||||
name='group_size_max'
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('products:Validation.group_size_max'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('products:use_dates')}
|
||||
name='use_dates'
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('products:Validation.use_dates'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<RangePicker presets={datePresets} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('products:Weekdays')}
|
||||
name='weekdayList'
|
||||
>
|
||||
<Checkbox.Group options={['5', '6', '7']} />
|
||||
</Form.Item>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductInfoQuotation
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue